本文来自: PerfMa技术社区

PerfMa(笨马网络)官网

概述

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

同一个类加载器对象是否能够加载同一个类文件屡次并且失去多个Class对象而都能够被java层应用吗

请认真留神下面的形容里几个要害的词

  • 同一个类加载器:意味着不是每次都new一个类加载器对象,我晓得有些对类加载器有点了解的同学必定会想到这点。咱们这里强调的是同一个类加载器对象去加载。
  • 同一个类文件:意味着类文件里的信息都统一,不存在批改的状况,至多名字不能改。因为有些同学会钻空子,比如说拿到类文件而后批改名字啥的,哈哈。
  • 多个Class对象:意味着每次创立都是新的Class对象,并不是返回同一个Class对象。
  • 都能够被java层应用:意味着Java层能感知到,或者对我公众号关注挺久的同学看过我的一些文章,晓得我这里说的是什么,不晓得的能够翻翻我后面的文章,这里卖个关子,不间接通知你哪篇文章,略微提醒一下和内存GC无关。

那接下来在看上面文章之前,我感觉你能够先思考一个问题,

同一类加载器对象是否可加载同一类文件屡次且失去多个不同的Class对象(单选)
A.不晓得 B.能够 C.不能够

尽管有些题目党的意思,不过我感觉题目里的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);    }

上述代码其实就是通过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里,因而咱们能够无穷次加载同一个类。这个对于绝大部分人来说是不太理解的,因而大家在面试的时候,你能讲清楚我这文章里的状况,置信是一个加分项,不过也可能被误伤,因为你的面试官也可能不分明这种状况,不过你能够通知他我这篇文章,哈哈,有播种请帮忙点个难看,并分享进来,感激。

一起来学习吧

PerfMa KO 系列课之 JVM 参数【Memory篇】

java内存溢出问题剖析过程