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

本文来自: PerfMa技术社区

PerfMa(笨马网络)官网java

概述

首先仍是把问题抛给你们,这个问题也是我厂同窗在作一个性能分析产品的时候碰到的一个问题。面试

同一个类加载器对象是否能够加载同一个类文件屡次而且获得多个Class对象而均可以被java层使用吗

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

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

那接下来在看下面文章以前,我以为你能够先思考一个问题,网络

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

虽然有些标题党的意思,不过我以为标题里的99.99%说得应该不夸张,这个比例或许应该更大,不过仍是请认真做答,不要随便选,我知道确定有人会随便选的,哈哈。数据结构

正常的类加载

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

那这么一说看起来不可能出现同一个类加载器加载同一个类屡次的状况。工具

正常状况下也确实是这样的。性能

奇怪的现象

然而咱们从java进程的内存结构里却看到过相似这样的一些现象,如下是咱们性能分析产品里的部分截图
image.png
在这个现象里,名字为java.lang.invoke.LambdaForm$BMH的类有多个,而且其类加载器都是BootstrapClassLoader,也就是同一个类加载器竟然加载了同一个类屡次。这是咱们的分析工具备问题吗?显然不是,由于咱们从内存里读到的就是这样的信息。学习

现象模拟

上面的这个现象看起来和lambda有必定关系,不过实际上并不只仅lambda才有这种状况,咱们能够来模拟一下spa

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内存溢出问题分析过程

相关文章
相关标签/搜索