文章目录
JVM类加载器
类加载的过程
我们用类加载(Class Loading)来表示: 将class/interface名称映射为Class对象的一整个过程。 这个过程还可以划分为更具体的阶段: 加载,链接和初始化(loading, linking and initializing)。
-
加载
阶段主要是获取class文件,加载阶段并不会检查class的语法和格式 -
校验
是链接的第一个阶段,确保class文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。校验过程检查 classfile 的语义,判断常量池中的符号,并执行类型检查。 -
准备
阶段将会创建静态字段, 并将其初始化为标准默认值(比如null 或者0值),并分配方法表,即在方法区中分配这些变量所使用的内存空间。请注意,准备阶段并未执行任何Java代码。例如:
public static int i = 1;
在准备阶段i 的值会被初始化为0,后面在类初始化阶段才会执行赋值为1。
但是下面如果使用final作为静态常量,某些JVM的行为就不一样了:
public static final int i = 1;
对应常量i,在准备阶段就会被赋值1。例如其他语言(C#)有直接的常量关键字const,让告诉编译器在编译阶段就替换成常量,类似于宏指令,更简单。
-
解析
主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析。当一个变量引用某个对象的时候,这个引用在.class 文件中是以符号引用来存储的(相当于做了一个索引记录)。在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。(我是这么理解的,我的脑袋里面装了很多知识,但是这些知识的具体细节我不知道,比如我学了Spring,你问我Spring这个方法是什么,Spring这个类有什么作用的时候,我并不知道,但是由于我系统地学习了Spring,对于知识的框架我大概知道,根据你的问题,我能知道这是哪一块的知识,然后对应去查找就行了)加载一个class时, 需要加载所有的super类和super接口口方法解析。
-
初始化
:JVM规范明确规定, 必须在类的首次主动使用时才能执行类初始化。到达了初始化
这个阶段,则说明这个类要被进行使用了。类加载并不代表这个类就要被初始化。就像把知识的索引先装进脑海中,只有等到要用的时候才准备拿出来,而不是一股脑的全部拿出来。
初始化的过程包括执行:类构造器方法
,static静态变量赋值语句
,static静态代码块
如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化。所以其实在java中初始化一个类,那么必然先初始化过java.lang.Object
类,因为所有的java类都继承自java.lang.Object
。为了提高性能,HotSpot JVM 通常要等到类初始化时才去装载和链接类。 因此,如果A类引用了B类,那么加载A类并不一定会去加载B类(除非需要进行验证)。主动对B类执行第一条指令时才会导致B类的初始化,这就需要先完成对B类的装载和链接。
类加载的时机
Class.forName
和ClassLoader
的示例代码
首先有一个类,我们分别使用两种方法来加载它。
package classloader;
public class ForName {
static {
System.out.println("ForName类静态代码块初始化");
}
private static String name = method();
private static String method() {
System.out.println("ForName类给静态变量赋值的静态方法执行");
return "initname";
}
}
使用Class.forName
加载类
package classloader;
public class ClassForNameTest {
public static void main(String[] args) throws Exception {
Class<?> aClass = Class.forName("classloader.ForName");
}
}
输出结果如下,可以看到类不仅加载了,还进行初始化了。
使用ClassLoader
加载类
package classloader;
public class ClassLoaderForNameTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("classloader.ForName");
}
}
没有任何输出,说明只是加载,但是并未进行初始化
也可以使用Class.forName
只加载不初始化,如下,将初始化参数设置为false。
package classloader;
public class ClassForNameTest {
public static void main(String[] args) throws Exception {
//getSystemClassLoader返回的是一个AppClassLoader
Class<?> aClass = Class.forName("classloader.ForName", false, ClassLoader.getSystemClassLoader());
}
}
输出如下
不同的类加载器
Java11中的ClassLoader的类继承关系
从JDK1.8之后的版本提供有一个PlatformClassLoader
类加载器,而在JDK1.8以及以前的版本里面提供的加载器为ExtClassLoader
,因为在JDK的安装目录里面提供又一个ext的目录,开发者可以将*.jar
文件拷贝到此目录里面,这样就可以直接执行了,但是这样的处理开发并不安全,最初的时候也是不提倡使用的,所以从JDK9开始将其彻底废除,同时为了和系统加载器与应用类加载器之间保持设计的平衡,提供有PlatformClassLoader
。
Java11中的ClassLoader的继承关系
Java8中的ClassLoader的继承关系
启动类加载器(BootstrapClassLoader)
启动类加载器(bootstrap class loader): 它用来加载 Java 的核心类,是用原生C++代码来实现的,并不继承自 java.lang.ClassLoader。它可以看做是JVM自带的,我们再代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它。举例来说,java.lang.String是由启动类加载器加载的,所以String.class.getClassLoader()
就会返回null。
在Java8中,可以通过如下代码打印出启动类加载器所加载的文件的路径。但是在Java8之后,则不允许了。通过下面的图片可以看出启动类加载器负责加载jre/classes
下的class文件以及jre/
下的jar包
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("BootStrap ClassLoader");
for (URL url : urls) {
System.out.println(" ==> " + url.toExternalForm());
}
Java8打印输出结果
扩展类加载器(ExtClassLoader)
扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,加载jre/lib/ext/
或者由java.ext.dirs
系统属性指定的目录中的JAR包的类,代码里直接获取它的父类加载器为null(因为无法拿到启动类加载器)。
注意:在Java8之后。提供有一个PlatformClassLoader
类加载器,而在JDK1.8以及以前的版本里面提供的加载器为ExtClassLoader
。因为在JDK的安装目录里面提供又一个ext的目录,开发者可以将*.jar
文件拷贝到此目录里面,这样就可以直接执行了,但是这样的处理开发并不安全,最初的时候也是不提倡使用的,所以从JDK9开始将其彻底废除,同时为了和系统加载器与应用类加载器之间保持设计的平衡,提供有PlatformClassLoader
。
应用类加载器(AppClassLoader)
应用类加载器(app class loader):它负责在JVM启动时加载来自Java命令的classpath
或者cp选项、java.class.path
系统属性指定的jar包和类路径。在应用程序代码里可以通过ClassLoader
的静态方法getSystemClassLoader()
来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。
以我的Maven项目为例:classpath
指的是src.main.java
和src.main.resources
以及第三方jar包的根路径
。我们自己编写的代码都位于classpath
中。
用户自定义类加载器
用户自定义了类加载器,则自定义类加载器
都以应用类加载器
作为父加载器。应用类加载器
的父类加载器为扩展类加载器
。这些类加载器是有层次关系的。
层次关系图
示例代码
首先有一个类Hello.java
package classloader;
public class Hello {
static {
System.out.println("Hello Class Initialized!");
}
public Hello() {
System.out.println("Constructor is invoked");
}
public void hello() {
System.out.println("Hello World! Hello Simon!");
}
}
先将Hello.java
编译为class
文件,然后使用base64
命令对class文件进行加密,然后将加密的文件存入一个hello.txt这个文件。
root@simon-computer:~/IdeaProjects/java-training/jvm/src/main/java/classloader# base64 Hello.class
yv66vgAAADQAIQoACAARCQASABMIABQKABUAFggAFwgAGAcAGQcAGgEABjxpbml0PgEAAygpVgEA
BENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAVoZWxsbwEACDxjbGluaXQ+AQAKU291cmNlRmlsZQEA
CkhlbGxvLmphdmEMAAkACgcAGwwAHAAdAQAWQ29uc3RydWN0b3IgaXMgaW52b2tlZAcAHgwAHwAg
AQAZSGVsbG8gV29ybGQhIEhlbGxvIFNpbW9uIQEAGEhlbGxvIENsYXNzIEluaXRpYWxpemVkIQEA
EWNsYXNzbG9hZGVyL0hlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0B
AANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJp
bnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAcACAAAAAAAAwABAAkACgABAAsAAAAtAAIA
AQAAAA0qtwABsgACEgO2AASxAAAAAQAMAAAADgADAAAACAAEAAkADAAKAAEADQAKAAEACwAAACUA
AgABAAAACbIAAhIFtgAEsQAAAAEADAAAAAoAAgAAAA0ACAAOAAgADgAKAAEACwAAACUAAgAAAAAA
CbIAAhIGtgAEsQAAAAEADAAAAAoAAgAAAAUACAAGAAEADwAAAAIAEA==
然后自定义classloader
读取txt文件,进行解密,并加载对应的class。
package classloader;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;
public class HelloClassLoader extends ClassLoader {
private String path;
public HelloClassLoader(String path) {
this.path = path;
}
public static void main(String[] args) {
//获取系统的当前路径为:D:\dev\Java\java-traning\jvm-learning
System.out.println(System.getProperty("user.dir"));
try {
Class<?> clazz = new HelloClassLoader("src\\main\\java\\classloader\\hello.txt").findClass("classloader.Hello");
Method declaredMethod = clazz.getDeclaredMethod("hello");
declaredMethod.invoke(clazz.getConstructor().newInstance());
} catch (ClassNotFoundException | IllegalAccessException |
InstantiationException | NoSuchMethodException |
InvocationTargetException e) {
e.printStackTrace();
}
}
protected String readBaseClassFromTxt() throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(this.path)));
String readStr = "";
String str;
//一次性读取一行
while ((str = reader.readLine()) != null) {
str = str.trim();
readStr += str;
}
readStr = readStr.trim();
return readStr;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String helloBase64 = null;
try {
helloBase64 = readBaseClassFromTxt();
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes = decode(helloBase64);
return defineClass(name, bytes, 0, bytes.length);
}
public byte[] decode(String base64) {
return Base64.getDecoder().decode(base64);
}
}
从输出可以看出自定义类加载器加载并初始化相关类。
类加载机制
类加载机制的特点
-
双亲委托:当类加载器需要加载一个类,比如要加载
java.lang.String
这个类,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载器如果发现自己还有父加载器,会一直继续往上委托,直至委托到启动类加载器。启动类加载器尝试去加载这个类,如果启动类加载器能加载这个类,比如java.lang.String
这个类启动类加载器是能加载的,因此它就会告诉它的子加载器,“我已经加载了,你就不用加载了”。然后子类加载继续告诉它的子类加载器:“这个类已经被加载了,你不用加载了”,这样,所有的子加载器都不需要自己加载了。
但如果启动类加载器无法加载,那么启动类加载的子加载器就会尝试去加载,如果启动类加载器的子加载器也无法进行加载,则启动类加载器的子加载器的子加载器则尝试去加载。如果所有类加载器都没有加载到指定名称的类,那么会抛出ClassNotFountException
异常。
-
负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
-
缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。
参考:极客时间Java训练营