类加载运行全过程
当我门用java命令运行如下代码类的main函数启动程序时,首先需要通过类加载器把主类加载到jvm当中。
package com.tuling.jvm;
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute(){ // 一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 100;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
通过Java命令执行代码的大体流程如下:
图中的loadClass的类加载过程有如下几步:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
-
加载:在磁盘上查找并通过IO读入字节码文件(寻找target目录下,com/tuling/jvm目录下的Math.class文件),使用到类时才会加载到内存,例如调用类的main()方法,new对象等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。总结来说就是把Math.class文件加载到内存区域中。
-
验证:校验字节码文件的正确性。校验Math.class文件是否正确。
-
准备:给类的静态变量分配内存,并赋予默认值。
比如public static int initData = 666; 会给initData变量赋值0
比如public static final int initData = 666; 有final修饰的直接赋值666,因为final修饰之后不属于变量,属于常量。 -
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载器间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
可以使用javap命令解析Math.class,反解析出当前类对应的字节码指令
反解析出如下指令
D:\Java\jdk1.8.0_201\bin\javap.exe -v com.tuling.jvm.Math
Classfile /D:/Desktop/PYWork/jvm-project/target/classes/com/tuling/jvm/Math.class
Last modified 2022-6-21; size 759 bytes
MD5 checksum eade08641619722a508b4836d44e378d
Compiled from "Math.java"
public class com.tuling.jvm.Math // 类的名称
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER // 访问修饰
Constant pool: // 常量池
#1 = Methodref #9.#34 // java/lang/Object."<init>":()V
#2 = Class #35 // com/tuling/jvm/Math
#3 = Methodref #2.#34 // com/tuling/jvm/Math."<init>":()V
#4 = Methodref #2.#36 // com/tuling/jvm/Math.compute:()I
#5 = Fieldref #2.#37 // com/tuling/jvm/Math.initData:I
#6 = Class #38 // com/tuling/jvm/User
#7 = Methodref #6.#34 // com/tuling/jvm/User."<init>":()V
#8 = Fieldref #2.#39 // com/tuling/jvm/Math.user:Lcom/tuling/jvm/User;
#9 = Class #40 // java/lang/Object
#10 = Utf8 initData
#11 = Utf8 I
#12 = Utf8 user
#13 = Utf8 Lcom/tuling/jvm/User;
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/tuling/jvm/Math;
#21 = Utf8 compute
#22 = Utf8 ()I
#23 = Utf8 a
#24 = Utf8 b
#25 = Utf8 c
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 math
#31 = Utf8 <clinit>
#32 = Utf8 SourceFile
#33 = Utf8 Math.java
#34 = NameAndType #14:#15 // "<init>":()V
#35 = Utf8 com/tuling/jvm/Math
#36 = NameAndType #21:#22 // compute:()I
#37 = NameAndType #10:#11 // initData:I
#38 = Utf8 com/tuling/jvm/User
#39 = NameAndType #12:#13 // user:Lcom/tuling/jvm/User;
#40 = Utf8 java/lang/Object
{
public static int initData;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static com.tuling.jvm.User user;
descriptor: Lcom/tuling/jvm/User;
flags: ACC_PUBLIC, ACC_STATIC
public com.tuling.jvm.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/tuling/jvm/Math;
public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 100
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 10: 0
line 11: 2
line 12: 4
line 13: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/tuling/jvm/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/tuling/jvm/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
8 6 1 math Lcom/tuling/jvm/Math;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: sipush 666
3: putstatic #5 // Field initData:I
6: new #6 // class com/tuling/jvm/User
9: dup
10: invokespecial #7 // Method com/tuling/jvm/User."<init>":()V
13: putstatic #8 // Field user:Lcom/tuling/jvm/User;
16: return
LineNumberTable:
line 5: 0
line 6: 6
}
SourceFile: "Math.java"
Process finished with exit code 0
比如看main方法,new之后指向了 #2 , #2指向的是常量池的指令Class,#2又指向#35,#35对应的是Math类
- 初始化:对类的静态变量初始化指定的值,执行静态代码块。
类被加载到方法区中后主要包括运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的class类型的对象实例放到堆中,作为开发人员访问方法区中类定义的入口和切入点。
注意,主类在运行过程中如果使用到其它类,会使用到时才会加载,加载机制其实可以算是为懒加载。
类加载器和双亲委派机制
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等。例如String等系统类
- 拓展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
- 自定义加载器:负责加载用户自定义路径下的包
看一个类加载器示例:
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
}
}
打印结果如下
String类属于系统类,使用的是引导类加载器,因为是c++创建的引导类加载器,所以打印结果是null
DESKeyFactory类在sunjce_provider.jar的jar包下,这个jar包在lib目录中的ext目录中,所以DESKeyFactory类使用EXTClassLoader(扩展类加载器)加载
TestJDKClassLoader类时自定义的类,所以使用的是应用程序类加载器。
如果看控制台打印结果发现EXTClassLoader和AppClassLoader都是sun.misc包的Launcher类的一部分。Launcher是jvm启动器里的非常核心的一个类,看最上边的流程图会发现,c++创建一个引导类加载器之后,c++代码会调用java代码中的Launcher类,这个类相当于jvm类加载器中的一个启动器。他初始化这个类之后,再会加载其他类。初始化Launcher过程中会把其他的类加载器都创建出来。
打开Launcher类源码,查看是如何初始化Launcher过程中创建其他类加载器。
下段代码是Launcher类的构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构造扩展类加载器,在构造的过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
//Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
总结来说类加载器初始化过程:
参加类允许加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。
sum.misc.Launcher初始化使用了单例设计模式,保证一个JVM虚拟机只有一个sun.misc.Launcher实例。
在Launcher构造方法内部,其创建了两个类加载器,分别sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。
讲解双亲委派机制之前类加载器的parent属性
创建appclassloader时传入了拓展类加载器
AppClassLoader的构造方法中传入了ClassLoader, 也就是上图中的拓展类加载器
点击super跳转到了URLCLassLoader类的URLClassLoader方法中,如下图所示
再点击super,跳转到了SecureCLassLoader类的SecureClassLoader方法中,如下图所示
再点击super,跳转到了ClassLoader类中的ClassLoader方法中
再继续往下,点击this,会跳转到ClassLoader类的私有构造方法,如下图所示,会发现最初创建应用来加载器时传入的拓展类加载器在这里赋值到了parent属性中。
双亲委派机制
JVM类加载器是有亲子层级结构的,如下图所示
这里类加载机制其实就有一个双亲委派机制,加载某个类时判断是否有父类加载器,如果有就委托父类加载器寻找目标,委托到父加载器之后,父加载器判断是否还有没有当前加载器的父加载器,若有就委托父加载器寻找目标,以此类推。如果所有的父加载器在自己的加载路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
比如我们的Math类,最先会找到应用程序类加载器,应用程序类加载器会委扩展展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径中没找到Math类,则向下退回Math类的加载请求,扩展类加载器就收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径中找到了Math类,结果就自己就加载了。
双亲委派机制说白了就是,先找到父加载器,不行再由儿子自己加载。
我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法大体逻辑如下:
- 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
- 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查当前类加载器是否已经加载过该类,加载过直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 如果当前类加载器的父加载器不为空则委托父加载器加载该类
c = parent.loadClass(name, false);
} else { // 如果当前类加载器的父加载器为空则委托引导类加载器加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) { // 不会执行
resolveClass(c);
}
return c;
}
}
为什么要设计双亲委派机制
沙箱安全机制:比如自己写的java.lang.String类不会被加载,因为最先会委托到引导类加载器加载目标类,这样自己写的String类和系统的String类由差异,代码会执行失败,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父加载器已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一行
自定义类加载器示例
自定义类加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法,一个是loadClass方法(实现了双亲委派机制), 另一个方法是findClass, 默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法
package com.tuling.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
public static void main(String[] args) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
}
打破双亲委派机制
打破双亲委派机制其实主要自定义的类加载器重写loadClass方法就可以,把loadClass方法中的委托父加载器的代码删除掉即可,但是我们自定义的类经常会使用系统包,所以双亲委派机制那段代码修改为如果是自定义的包直接使用自定义的类加载器,其他的包还是委托父加载器加载。代码如下
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
long t1 = System.nanoTime();
if (!name.startsWith("com.tuling.jvm")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}