写点什么

大厂码农涨薪 10k 的秘诀:JVM 的类加载机制你是否理解到这个程度?

发布于: 2021 年 05 月 16 日
大厂码农涨薪10k的秘诀:JVM的类加载机制你是否理解到这个程度?

今日分享开始啦,请大家多多指教~

1. 类加载阶段

1.1 加载阶段

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用

  • _super 即父类

  • _fields 即成员变量

  • _methods 即方法

  • _constants 即常量池

  • _class_loader 即类加载器

  • _vtable 虚方法表

  • _itable 接口方法表

如果这个类还有父类没有加载,则先触发父类的加载。

加载和链接可能是交替运行的。

注意:

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中

  • 可以通过前面介绍的 HSDB 工具查看

1.2 链接阶段

验证

验证类是否符合 JVM 规范,安全性检查,阻止不合法的类继续运行。用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行:

准备

为 static 变量分配空间,设置默认值:

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾

  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

  • 将常量池中的符号引用解析为直接引用

解析

将常量池中的符号引用解析为直接引用:

1.3 初始化阶段

< init()> V 方法

初始化即调用 < cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全。

发生的时机

概括地说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时

  • 子类初始化,如果父类还没初始化,会引发

  • 子类访问父类的静态变量,只会触发父类的初始化

  • Class.forName

  • new 会导致初始化

不会导致类初始化的情况:

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

  • 类对象.class 不会触发初始化

  • 创建该类的数组不会触发初始化

  • 类加载器的 loadClass 方法

测试代码:

验证(测试时请先全部注释,每次只执行其中一个)


1.4 练习

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化:

典型应用 - 完成懒惰初始化单例模式:

以上的实现特点是:

  • 懒惰实例化

  • 初始化时的线程安全是有保障的

2. 类加载器

以 JDK 8 为例:

类加载器的优先级(由高到低):启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器

2.1 启动类加载器

用 Bootstrap 类加载器加载类:

执行:

输出:

-Xbootclasspath 表示设置 bootclasspath

其中 /a:. 表示将当前目录追加至 bootclasspath 之后

可以有以下几个方式替换启动类路径下的核心类:

  • java -Xbootclasspath: < new bootclasspath>

  • 前追加:java -Xbootclasspath/a:<追加路径>

  • 后追加:java -Xbootclasspath/p:<追加路径>

2.2 扩展类加载器

程序执行:

输出结果:

写一个同名的类:

打个 jar 包:

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext(扩展类加载器加载的类必须是以 jar 包方式存在),重新执行 Load5_2

输出:

2.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。

注意:这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系


例如:

执行流程为:

  • sun.misc.Launcher$AppClassLoader // 1 处, 开始查看已加载的类,结果没有

  • sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()

  • sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  • sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找

  • BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  • sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 处

  • 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

2.4 线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗? 让我们追踪一下源码:

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,回到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:


先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此 可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定命名为文件,文件内容是实现类名称

这样就可以使用:

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC

  • Servlet 初始化器

  • Spring 容器

  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:

2.5 自定义类加载器

问问自己,什么时候需要自定义类加载器:

1)想加载非 classpath 随意路径中的类文件

2)都是通过接口来使用实现,希望解耦时,常用在框架设计

3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  • 继承 ClassLoader 父类

  • 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制

  • 读取类文件的字节码

  • 调用父类的 defineClass 方法来加载类

  • 使用者调用该类加载器的 loadClass 方法


今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

Java工具与相关资料获取等WX: pfx950924(备注来源)

评论

发布
暂无评论
大厂码农涨薪10k的秘诀:JVM的类加载机制你是否理解到这个程度?