深圳幻海软件技术有限公司 欢迎您!

据说99.99%的人都会答错的类加载问题

2023-02-26

概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗?请仔细注意上面的描述里几个关键的词:同一个类加载器:意味着不是每次都new一个类加载器对象,我知道有些对类加载

概述

首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。

同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗?

请仔细注意上面的描述里几个关键的词:

  • 同一个类加载器:意味着不是每次都new一个类加载器对象,我知道有些对类加载器有点理解的同学肯定会想到这点。我们这里强调的是同一个类加载器对象去加载。
  • 同一个类文件:意味着类文件里的信息都一致,不存在修改的情况,至少名字不能改。因为有些同学会钻空子,比如说拿到类文件然后修改名字啥的,哈哈。
  • 多个Class对象:意味着每次创建都是新的Class对象,并不是返回同一个Class对象。
  • 都可以被java层使用:意味着Java层能感知到,或许对我公众号关注挺久的同学看过我的一些文章,知道我这里说的是什么,不知道的可以翻翻我前面的文章,这里卖个关子,不直接告诉你哪篇文章,稍微提示一下和内存GC有关。

虽然有些标题党的意思,不过我觉得标题里的99.99%说得应该不夸张,这个比例或许应该更大,不过还是请认真作答,不要随便选,我知道肯定有人会随便选的,哈哈。

正常的类加载

这里提正常的类加载,也是我们大家理解的类加载机制,不过我稍微说得深一点,从JVM实现角度来说一下。在JVM里有一个数据结构叫做SystemDictonary,这个结构主要就是用来检索我们常说的类信息,这些类信息对应的结构是klass,对SystemDictonary的理解,可以认为就是一个Hashtable,key是类加载器对象+类的名字,value是指向klass的地址。这样当我们任意一个类加载器去正常加载类的时候,就会到这个SystemDictonary中去查找,看是否有这么一个klass可以返回,如果有就返回它,否则就会去创建一个新的并放到结构里,其中委托类加载过程我就不说了。

那这么一说看起来不可能出现同一个类加载器加载同一个类多次的情况。

正常情况下也确实是这样的。

奇怪的现象

然而我们从java进程的内存结构里却看到过类似这样的一些现象,以下是我们性能分析产品里的部分截图:

在这个现象里,名字为java.lang.invoke.LambdaForm$BMH的类有多个,并且其类加载器都是BootstrapClassLoader,也就是同一个类加载器居然加载了同一个类多次。这是我们的分析工具有问题吗?显然不是,因为我们从内存里读到的就是这样的信息。

现象模拟

上面的这个现象看起来和lambda有一定关系,不过实际上并不仅仅lambda才有这种情况,我们可以来模拟一下

public static void main(String args[]) throws Throwable { 
       Field f = Unsafe.class.getDeclaredField("theUnsafe"); 
       f.setAccessible(true); 
       Unsafe unsafe = (Unsafe) f.get(null); 
       String filePath = "/Users/nijiaben/AA.class"
       byte[] buffer =getFileContent(filePath); 
       Class<?> c1 = unsafe.defineAnonymousClass(UnsafeTest.class, buffer, null); 
       Class<?> c2 = unsafe.defineAnonymousClass(UnsafeTest.class, buffer, null); 
       System.out.println(c1 == c2); 
   } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

上述代码其实就是通过Unsafe这个对象的defineAnonymousClass方法来加载同一个类文件两遍得到两个Class对象,最终我们输出为false。这也就是说c1和c2其实是两个不同的对象。

因为我们的类文件都是一样的,也就是字节码里的类名也是完全一样的,因此在jvm里的类对象的名字其实也都是一样的。不过这里我要提一点的是,如果将c1和c2的名字打印出来,会发现有些区别,分别会在类名后面加上一个/hashCode值,这个hash值是对应的Class对象的hashCode值。这个其实是JVM里的一个特殊处理。

另外你无法通过java层面的其他api,比如Class.forName来获取到这种class,所以你要保存好这个得到的Class对象才能后面继续使用它。

defineAnonymousClass的解说

defineAnonymousClass这个方法比较特别,从名字上也看得出,是创建了一个匿名的类,不过这种匿名的概念和我们理解的匿名是不太一样的。这种类的创建通常会有一个宿主类,也就是***个参数指定的类,这样一来,这个创建的类会使用这个宿主类的定义类加载器来加载这个类,最关键的一点是这个类被创建之后并不会丢到上述的SystemDictonary里,也就是说我们通过正常的类查找,比如Class.forName等api是无法去查到这个类是否被定义过的。因此过度使用这种api来创建这种类在一定程度上会带来一定的内存泄露。

那有人就要问了,看不到啥好处,为啥要提供这种api,这么做有什么意义,大家可以去了解下JSR292。jvm通过InvokeDynamic可以支持动态类型语言,这样一来其实我们可以提供一个类模板,在运行的时候加载一个类的时候先动态替换掉常量池中的某些内容,这样一来,同一个类文件,我们通过加载多次,并且传入不同的一些cpPatches,也就是defineAnonymousClass的第三个参数, 这样就能做到运行时产生不同的效果。

主要是因为原来的JVM类加载机制是不允许这种情况发生的,因为我们对同一个名字的类只能被同一个类加载器加载一次,因而为了能支持动态语言的特性,提供类似的api来达到这种效果。

总结

总的来说,正常情况下,同一个类文件被同一个类加载器对象只能加载一次,不过我们可以通过Unsafe的defineAnonymousClass来实现同一个类文件被同一个类加载器对象加载多遍的效果,因为并没有将其放到SystemDictonary里,因此我们可以无穷次加载同一个类。这个对于绝大部分人来说是不太了解的,因此大家在面试的时候,你能讲清楚我这文章里的情况,相信是一个加分项,不过也可能被误伤,因为你的面试官也可能不清楚这种情况。

【本文是51CTO专栏作者李嘉鹏的原创文章,转载请通过微信公众号(你假笨,id:lovestblog)联系作者本人获取授权】 

戳这里,看该作者更多好文