类加载
1、类加载子系统
在介绍类的加载过程之前,先看看类加载子系统的组成。

- 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
2、类的加载过程
2.1 类的加载过程一:Loading
- 1.首先通过一个类的全限定名来获取此类的二进制字节流。
- 2.其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 3.最后在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.2 类的加载过程二:Linking
1.验证:确保被加载类的正确性,不危害虚拟机的安全。主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
2.准备:
- 为类变量(也叫静态变量)分配内存并设置该类变量的默认初始值,即零值。
- 如果用final变量修饰static,由于final在编译时就会被分配了,因而在准备阶段会被显示初始化。
- 这里不会为实例变量分配初始化,因为此时对象还没被创建,类变量会分配在方法区中,而实例变量会随着对象被分配到Java堆中。
1
2
3
4
5
6
7public class LinkingTest {
private static int a = 1;//a在准备阶段被赋值为0,在初始化阶段才被赋值为1
private final static int b = 1;//b在准备阶段赋值为1
private int c;//c在准备阶段不会被初始化
public static void main(String[] args) {
}
}3.解析:
- 把常量池中的符号引用转换为直接引用。符号引用就是一组符号来描述所引用的目标,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位表目标的句柄。如在执行完javap -v HelloApp.class反编译.class文件后把这些符号引用转换为直接引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Fieldref #5.#26 // com/company/java01/HelloApp.a:I
#4 = Methodref #27.#28 // java/io/PrintStream.println:(I)V
#5 = Class #29 // com/company/java01/HelloApp
#6 = Class #30 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable- 事实上,解析操作往往伴随着JVM在执行完初始化后再执行。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
2.3 类的加载过程三:Initialization
- 为类的静态变量赋予正确的初始值 。
- 初始化阶段就是执行类构造器方法
()的过程。 ()方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
1 | public class ClassInitTest { |
查看
1 | 0 iconst_1 |
- 构造器方法中指令按语句在源文件中出现的顺序执行。
1 | public class InitTest { |
从字节码文件中的
1 | 0 iconst_1 |
()不同于类的构造函数(构造器是虚拟机视角的 ())。当没在类中写构造函数时,则使用的是默认的构造器,从字节码中可看到 只调用了父类Object的构造器:
1 | 0 aload_0 |
如果写了自定义的构造器,那么在
1 | public class InitTest2 { |
对应的
1 | 0 aload_0 |
因为此类中没有静态变量,所以字节码文件中自然也不会有
- 若该类具有父类,JVM会保证子类的
()执行前,父类的 ()已经执行完毕。
1 | public class ClinitTest1 { |
son类的字节码文件中的
1 | 0 getstatic #2 <com/company/java01/ClinitTest1$Son.A>//父类已经加载过 |
- 虚拟机必须保证一个类的
()方法在多线程下被同步加锁。
1 | public class ThreadTest { |
输出结果为:
1 | 线程一开始 |
可见线程一首先抢到DeadThread的调用,因为DeadThread类中有死循环导致线程一出不来,此时线程二也无法调用DeadThread了。一个类只会被加载一次,加载后会被放在方法区缓存起来,即类在加载时只会调用一次
3、加载器
- 从上面可以知道,类加载器基本职责就是根据类的二进制名(binary name)读取java编译器编译好的字节码文件(.class文件),并且转化生成一个java.lang.Class类的一个实例。这样的每个实例用来表示一个Java类,jvm就是用这些实例来生成java对象的。基本上所有的类加载器都是java.lang.ClassLoader 类的一个实例。
- 总的来说,jvm支持两种类型的类加载器,分别为引导类加载器和自定义类加载器(jvm规范中将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器)。
1 | public class ClassLoaderTest { |
3.1 引导类加载器(启动类加载器):BootstrapClassLoader
- 这个类加载器是使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载java的核心类库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容等),用于提供JVM自身需要的类。
- 引导类加载器具体加载哪些核心代码可以通过获取值为 “sun.boot.class.path” 的系统属性获得。
- 它不是java原生代码编写的,所以其也不是java.lang.ClassLoader类的实例,其没有getParent方法。
- 它加载拓展类和应用程序类加载器,并指定为它们的父类加载器。
- 出于安全考虑,引导类加载器只加载包名为java、javax、sun等开头下的类。
通过运行System.out.println(System.getProperty(“sun.boot.class.path”))可得到如下信息:
1 | /opt/jdk1.8.0_202/jre/lib/resources.jar; |
1 | public class ClassLoaderTest { |
3.2 拓展类加载器:ExtensionClassLoader
- 拓展类加载器用来加载jvm实现的一个拓展目录,该目录下的所有java类都由此类加载器加载。此路径可以通过获取”java.ext.dirs”的系统属性获得。
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于java.lang.ClassLoader,即为ClassLoader类的一个实例。
- 父类加载器为启动类加载器(引导类加载器)。
- 如果用户创建的jar文件在/jre/lib/ext目录(拓展目录)下,也会自动由拓展类加载器加载。
其有关类继承关系如下所示:
- 通过运行System.out.println(System.getProperty(“java.ext.dirs”))可得到如下信息:
1 | /opt/jdk1.8.0_202/jre/lib/ext; |
1 | public class ClassLoaderTest { |
3.2 应用类加载器(系统类加载器):AppClassLoader
- 应用类加载器又称为系统类加载器,开发者可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现。
- 派生于java.lang.ClassLoader,即为ClassLoader类的一个实例。
- 它负责加载环境变量classpath或系统属性”java.class.path”指定路径下的类库。
- 它是程序中默认的类加载器,一般来说,java应用都是用此类加载器完成加载的。
- 父类加载器为拓展类加载器。
其有关类继承关系如下所示:
- 通过运行System.out.println(System.getProperty(“java.class.path”))可得到如下信息:
1 | /opt/jdk1.8.0_202/jre/lib/charsets.jar |
3.3 用户自定义类加载器
- 在Java的日常开发中,类几乎是由上面三种加载器配合加载的,但在必要时还可以自定义类加载器,来定制类的加载方式。
- 为什么要自定义类加载器?
- 隔离加载类
- 修改类的加载方式
- 扩展加载源
- 防止源码泄露(对字节码文件进行加密后要运行时需解密,可以自定义类加载器解密)
- 用户自定义类加载器实现步骤:
- 继承抽象类ClassLoader。
- 在jdk1.2之前需要继承抽象类ClassLoader并重写loadClass()方法,但在jdk1.2后不再建议此方法,而是建议把自定义类加载逻辑写在findClass()方法中。
1 | public class CustomClassLoader extends ClassLoader { |
- 如果自定义类加载器没有过多复杂的需求,可以直接继承URLClassLoader类,这样可以避免重写findClass()方法以及获取字节码流的方式,使自定义类加载器编写更加简洁。
3.4 关于ClassLoader
- 所有的类加载器(除了启动类加载器)都继承于抽象类ClassLoader。
- 这个类加载器的一些核心方法:
| 方法名 | 说明 |
|---|---|
| getParent() | 返回该类加载器的父类加载器 |
| loadClass(String name) | 加载名为name的类,返回java.lang.Class类的实例 |
| findClass(String name) | 查找名字为name的类,返回的结果是java.lang.Class类的实例 |
| findLoadedClass(String name) | 查找名字为name的已经被加载过的类,返回的结果是java.lang.Class类的实例 |
| defineClass(String name,byte[] b,int off,int len) | 根据字节数组b中的数据转化成Java类,返回的结果是java.lang.Class类的实例 |
获取类加载器途径:
- 获取当前类的ClassLoader。
1
2
3
4
5
6
7
8public static void main(String[] args) {
try {
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader); //null
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}- 获取当前线程上下文的ClassLoader。
1
2
3
4
5
6
7
8
9
10public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(contextClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}- 获取系统的ClassLoader。
1
2
3
4
5
6
7
8
9
10public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println(systemClassLoader);//sun.misc.Launcher$ExtClassLoader@74a14482
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}- 获取调用者的ClassLoader(DriverManager.getCallerClassLoader())。
3.5 双亲委派机制
JVM对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,JVM使用的是双亲委派模式,即把请求交由父类加载器处理,它是一种任务委派模式。
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父加载器去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,请求最终达到顶层的启动类加载器。
- 如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。
先在java.lang包下新建自定义的String类:
1 | package java.lang; |
然后进行引用,进行类加载:
1 | package com.jvm.demo; |
运行后发现自定义类中的static代码块并没有执行,原因是双亲委派机制,前面说过引导类加载器负责加载java、javax、sun开头的包下的类,由于该自定义类在java包下,在向上委托的过程中交给了引导类加载器加载,所以实际加载的是java核心类库内的String类。
而通过打印下方StringTest类的加载器类型,可以知道是预期的sun.misc.Launcher$AppClassLoader,是因为它在com.jvm.demo包下,并不归应用类加载器上层的拓展类加载器和引导类加载器加载。
1 | package com.jvm.demo; |
- 如果在自定义String类中定义main方法并运行:
1 | package java.lang; |
会发现报错如下:
1 | 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: |
更加确切地说明加载的String类是核心代码库里的String类,而里面并没有main()方法,因而报错,从而保证了无法随意篡改类定义。
- 如果在java.lang包下自定义一个核心api没有的类:
1 | package java.lang; |
运行后会发现报错如下:
1 | Error: A JNI error has occurred, please check your installation and try again |
原因是虽然此类在双亲委派机制中交给了引导类加载器加载,但是出于安全考虑,访问java.lang包下的类是需要权限的,它阻止我们用此包名去定义自定义类。试想如果加载这个自定义类成功了,可能会对引导类加载器造成破坏,保证了安全。
- 双亲委派机制的好处:
- 避免类的重复加载。
- 保护程序安全,防止核心API被随意篡改,即沙箱安全机制。
3.6 类的主动和被动使用
- 类的主动使用分为以下几种情况,其它情况均视为被动使用:
- 创建类的实例。
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法。
- 反射(如Class.forName(“com.bunny.Test”))。
- 初始化一个类的子类。
- Java虚拟机启动时被表明为启动类的类(JavaTest)。
- Jdk7开始提供的动态语言支持。
1 | public class ClassUsed { |