本文是学习尚硅谷宋红康老师主讲的尚硅谷JVM精讲与GC调优教程的总结(文末有链接)
本篇可能被问到的问题:
- 类的加载过程
- 类加载器
- 自定义类的加载器、ClassLoader
- 双亲委派机制,破坏此机制的例子
类的加载过程(生命周期)
基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
一个类只能被同 1 个类的加载器加载 1 次。
类的加载过程是前三个过程,生命周期多了后两个过程。
Loading (装载)阶段
- 此阶段做的事
查找并加载类的二进制数据,生成 Class 的实例。
- 加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(元空间)。
Class 实例存储在堆中。Class 类的构造方法是私有的,只有 JVM 能够创建。
- 数组类的加载
数组类本身不是由类加载器负责创建,而是由 JVM 在运行时根据需要直接创建的,但数值的元素类型如果是引用类型的话需要依靠类加载器去创建。
Linking (链接)阶段
Verification (验证)
目的是保证加载的字节码是否合法、合理并符合规范的。(不用深究)
Preparation(准备)
为类的静态变量分配内存,并将其初始化为默认值。
假如有个类有如下静态变量:
private static int id = 1;
则在准备阶段只会把 id
初始化为 int
类型的默认值 0,在初始化阶段才会将其赋值为 1 。
有个例外是基本数据类型的字段用 static final 修饰的情况,准备阶段会显示赋值,因为 final 在编译的时候就会分配了。例如下面的 ID 会在准备阶段显示赋值为 1:
private static final int ID = 1
Resolution (解析)
将类、接口、字段和方法的符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。
符号引用是和内存没有关系的,是字节码文件中的引用,例如 info#24
。
直接引用是真实在内存中的引用。
解析操作往往会伴随着 JVM 在执行完初始化之后再执行。
Initialization (初始化)阶段
- 此阶段做的事
为类的静态变量赋予正确的初始值(显示初始化)。
例如如下代码中将 id 赋值为 1 ,number 赋值为 2, C 赋值为 1000。
public class A {
private static int id = 1;
private static int number;
private static final Integer C = Integer.valueOf(1000);
static {
number = 2;
}
}
此阶段的重要工作是执行类的初始化方法:() 方法。static final 修饰的常量必须是调用方法赋值(不是常量),才会生成 () 方法。
-
子类加载前先加载父类。由父及子,静态先行。
-
clinit的调用可能会调用死锁。
clinit是带锁线程安全的。
jps、jstack {pid} 查看
- 类的加载时机
- Class 的 forName(“Java.lang.String”) 和 Class 的 getClassLoader 的 loadClass(“Java.lang.String”) 有什么区别?
forName(“Java.lang.String”) 会执行装载、链接和初始化,loadClass 只会执行 装载。
- 类的构造函数执行之前会先执行成员变量的显示赋值和构造块。
类的 Using (使用)
开发人员在程序中访问和调用类的静态成员信息(静态字段、静态方法),或者使用 new 关键字为其创建对象实例。
类的 Unloading (卸载)
一般只有自定义类加载器加载的类才会被卸载。
类的加载器
-
类的加载器分为两类:
- 引导类加载器 (Bootstrap ClassLoader),使用 C、C++ 语言编写。
- 自定义类加载器 (User-defined Classloader),除了引导类加载器之外的,使用 Java 语言编写,继承抽象类 ClassLoader 类。
-
图中常见类的加载器各自负责加载哪些类
- Bootstrap ClassLoader:主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/jre/lib目录下的 rt.jar 、resources.jar 、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
- Extension ClassLoader:主要负责加载 %JRE_HOME%/jre/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
- Application ClassLoader:负责加载当前应用 classpath 下的所有 jar 包和类。
-
图中的父类不是继承的关系,是通过组合实现的,成员变量 partent 指向上一级的加载器。
-
让应用程序打印类加载信息:
X:+TraceClassLoading 或者 -verbose:class java -XX:+TraceClassLoading Main java -verbose:class Main
-
每个 Class 对象都会包含一个定义它的 ClassLoader 的一个引用,不过,数组类不是通过 ClassLoader 创建的,而是通过 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
获取 ClassLoader 的途径:
- 获取当前类的 ClassLoader:clazz.getClassLoader()
- 获取当前线程上下文的 ClassLoader:Thread.currentThread().getContextClassLoader()
- 获取系统的 ClassLoader:ClassLoader.getSystemClassLoader()
- 获取当前 ClassLoader 的上层 ClassLoader:classLoader.getParent()
ClassLoader源码剖析
- java.lang.ClassLoader
- 方法 java.lang.ClassLoader#loadClass(java.lang.String, boolean)
体现了双亲委派机制
- 方法 java.lang.ClassLoader#findClass
根据类的二进制名称来查找类
- java.lang.ClassLoader#defineClass(java.lang.String, byte[], int, int)
将字节数组转换成 Class 实例,自定义类加载器时一般会调用这个方法
- java.lang.ClassLoader#preDefineClass
会对 java 开头的类进行保护,确保 JDK 核心 API 都是有 Bootstap ClassLoader 加载
-
sun.misc.Launcher.AppClassLoader
应用程序类加载器,sun.misc.Launcher 的静态内部类
-
sun.misc.Launcher.ExtClassLoader
扩展类加载器,sun.misc.Launcher 的静态内部类
自定义类的加载器
实现方式
- 继承 ClassLoader 类
- 继承后有两种做法
- 方式一:重写 loadClass() 方法 (打破双亲委派机制)
- 方式二:重写 findClass() 方法 (遵循双亲委派机制,推荐)
相关机制
双亲委派机制
是什么
类加载器在进行类加载的时候,首先把请求委派给父类加载器去完成,这样所有的请求都会委派给最顶层的启动类加载器。
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
优势
- 避免类的重复加载,确保一个类的全局唯一性
- 保护程序安全,防止核心 API 被随意篡改
劣势
- 顶层的 ClassLoader 类无法访问底层的 ClassLoader 所加载的类,即系统类访问应用类会有问题。
解决方法:线程上下文类加载器 Thread.currentThread().getContextClassLoader()
破坏双亲委派机制的场景
Tomcat 类加载机制
Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委派机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。
Bootstrap
|
System
|
Common
/ \
Server Shared
/ \
Webapp1 Webapp2 ...
沙箱安全机制
JDK 9 中类加载结构的新变化
参考
- 尚硅谷JVM精讲与GC调优教程(宋红康主讲,含jvm面试真题)
- JavaGuide 公众号:携程一面:什么是双亲委派模型
- Tomcat官方文档