文章目录[隐藏]
类加载
1. 类文件结构
根据 JVM 规范,类文件结构如下:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
1.1 魔数
0~3 字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
1.2 版本
4~7 字节,表示类的版本
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
00 00 ---表示小版本
00 34 ---主版本,表示52,代表JDK8
1.3 常量池
1.4 访问标识与继承信息
1.5 Field 信息
1.6 Method 信息
2. 类加载阶段
2.1 加载
- 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 instanceKlass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和连接可能是交替运行的
注意:
- instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
- 可以通过前面介绍的 HSDB 工具查看
2.2 验证
验证类是否符合 JVM规范,安全性检查,大致分为以下四个阶段:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
2.3 准备
为static变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型(new),那么赋值也会在初始化阶段完成
2.4 解析
将常量池中的符号引用解析为直接引用
/**
* 解析的含义
*
* @author Enndfp
*/
public class Demo1 {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Demo1.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classLoader.loadClass("com.enndfp.class_loader_test.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
2.5 初始化
<clinit>()
方法
初始化即调用<clinit>()
,虚拟机会保证这个类的『构造方法』的线程安全
2.5.1 会发生初始化
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
2.5.2 不会发生初始化
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
实验
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
验证(实验时请先全部注释,每次只执行其中一个)
public class Demo2 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("com.enndfp.class_loader_test.B");
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("com.enndfp.class_loader_test.B", false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName("com.enndfp.class_loader_test.B");
}
}
3. 类加载器
3.1 基本介绍
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)
类与类加载器
类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远超类加载阶段。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
第一遍自下而上询问有没有加载,第二遍自上而下看自己加载目录有没有,如果有,则加载;没有,则看下一级加载器
3.2 启动类加载器
用 Bootstrap 类加载器加载类:
package com.enndfp.class_loader_test;
public class F {
static {
System.out.println("bootstrap F init");
}
}
执行
package com.enndfp.class_loader_test;
/**
* 验证启动类加载器
*
* @author Enndfp
*/
public class Demo3 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.enndfp.class_loader_test.F");
System.out.println(aClass.getClassLoader());
}
}
输出
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<追加路径>
java -Xbootclasspath/p:<追加路径>
3.3 扩展类加载器
package com.enndfp.class_loader_test;
public class G {
static {
System.out.println("classpath G init");
}
}
执行
/**
* 验证扩展类加载器
*
* @author Enndfp
*/
public class Demo4 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.enndfp.class_loader_test.G");
System.out.println(aClass.getClassLoader());
}
}
输出
写一个同名的类
package com.enndfp.class_loader_test;
public class G {
static {
System.out.println("ext G init");
}
}
打个 jar 包
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext
重新执行 Demo4
输出
3.4 双亲委派模型
工作流程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
注意
这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//若找不到,抛出ClassNotFoundException异常
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
例如:
/**
* 验证双亲委派模型加载过程
*
* @author Enndfp
*/
public class Demo5 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Demo5.class.getClassLoader().loadClass("com.enndfp.class_loader_test.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
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 下查找,找到了
优势:
- 避免类被重复加载,确保类的全局唯一性
- 保护程序安全,防止核心api被随意篡改
弊端:
- 由于加载范围的限制,顶层的ClassLoader无法访问底层的ClassLoader所加载的类
3.5 破坏双亲委派模型
第一次破坏
在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。
而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。
这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写 loadClass,重写了之后就可以破坏委派逻辑了。
第二次破坏
双亲委派模型的第二次破坏是由其自身的缺陷引起的,为了解决基础类型调用用户代码的问题
引入了线程上下文类加载器,通过setContextClassLoader()
默认情况就是应用程序类加载器,然后利用Thread.currentThread().getContextClassLoader()
获得类加载器来加载
虽然违背了双亲委派模型的原则,但在特定情况下是必要的。后来,在JDK 6时,JDK提供了java.util.ServiceLoader
类,以META-INF/services中的配置信息,辅以责任链模式
第三次破坏
双亲委派模型的第三次破坏是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
OSGI 就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托,有很多平级之间的类加载器查找
3.6 自定义类加载器
3.6.1 使用场景
- 想加载非 classpath 随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
3.6.2 步骤
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass 方法
- 注意不是重写 loadClass 方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法
/**
* 自定义类加载器
*
* @author Enndfp
*/
public class MyClassLoader extends ClassLoader {
/**
* @param name 类名称
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "D:\\myclasspath\\" + name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}