关于java:据说9999的人都会答错的类加载的问题

2次阅读

共计 2808 个字符,预计需要花费 8 分钟才能阅读完成。

本文来自: 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 内存溢出问题剖析过程

正文完
 0