写点什么

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

用户头像
AI乔治
关注
发布于: 2020 年 10 月 20 日
据说99.99%的人都会答错的类加载的问题

概述

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

同一个类加载器对象是否可以加载同一个类文件多次并且得到多个 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 里,因此我们可以无穷次加载同一个类。这个对于绝大部分人来说是不太了解的,因此大家在面试的时候,你能讲清楚我这文章里的情况,相信是一个加分项,不过也可能被误伤,因为你的面试官也可能不清楚这种情况,不过你可以告诉他我这篇文章,哈哈,有收获请帮忙点个好看,并分享出去,感谢。


看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java 烂猪皮 』,不定期分享原创知识。

  3. 同时可以期待后续文章 ing🚀




本文作者:你假笨

出处:https://club.perfma.com/article/282222


用户头像

AI乔治

关注

分享后端技术干货。公众号【 Java烂猪皮】 2019.06.30 加入

一名默默无闻的扫地僧!

评论 (1 条评论)

发布
用户头像
技术文章搞这样的标题不无聊吗?
据说该文作者 99% 的时间都在吃屎
2020 年 10 月 20 日 23:24
回复
没有更多了
据说99.99%的人都会答错的类加载的问题