运行环境:
下面说明一下我的运行环境。我是在mac上操作的. 先找到mac的java地址. 从~/.bash_profile中可以看到
java的home目录是: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home
chatgpt免费体验:http://www.chat136.com
chatgpt学习:http://me.chat136.com
![](https://img-blog.csdnimg.cn/img_convert/df63c525a6aec5365d6732dd828ad722.png)
一. 类加载的过程
1.1 类加载器初始化的过程
假如现在有一个java类 com.lxl.jvm.Math类, 里面有一个main方法
package com.lxl.jvm;
publicclassMath {
publicstaticintinitData=666;
publicstaticUseruser=newUser();
publicintcompute() {
inta=1;
intb=2;
intc= (a + b) * 10;
return c;
}
publicstaticvoidmain(String[] args) {
Mathmath=newMath();
math.compute();
}
}
这个方法很简单, 通常我们直接执行main方法就ok, 可以运行程序了, 那么点击运行main方法, 整个过程是如何被加载运行的呢? 为什么点击执行main, 就能得到结果呢?
先来看看答题的类加载流程(宏观流程), 如下图:
![](https://img-blog.csdnimg.cn/img_convert/3c1b7923d81a26f33ea1c6d42a23cf22.png)
备注:
1. windows上的java启动程序是java.exe, mac下是java
2. c语言部分,我们做了解, java部门是需要掌握的部分.
第一步: java调用底层的jvm.dll文件创建java虚拟机(这一步由C++实现) . 这里java.exe是c++写的代码, 调用的jvm.dll也是c++底层的一个函数. 通过调用jvm.dll文件(dll文件相当于java的jar包), 会创建java虚拟机. java虚拟机的启动都是c++程序实现的.
第二步:在启动虚拟机的过程中, 会创建一个引导类加载器的实例. 这个引导类的加载器是C语言实现的. 然后jvm虚拟机就启动起来了.
第三步: 接下来,C++语言会调用java的启动程序.刚刚只是创建了java虚拟机, java虚拟机里面还有很多启动程序. 其中有一个程序叫做Launcher. 类全称是sun.misc.Launcher. 通过启动这个java类, 会由这个类引导加载器加载并创建很多其他的类加载器. 而这些加载器才是真正启动并加载磁盘上的字节码文件.
第四步:真正的去加载本地磁盘的字节码文件,然后启动执行main方法.(这一步后面会详细说,到底是怎么加载本地磁盘的字节码文件的。)
第五步:main方法执行完毕, 引导类加载器会发起一个c++调用, 销毁JVM
以上就是启动一个main方法, 这个类加载的全部过程
下面, 我们重点来看一下, 我们的类com.lxl.Math是怎么被加载到java虚拟机里面去的?
1.2 类加载的过程
上面的com.lxl.jvm.Math类最终会生成clas字节码文件. 字节码文件是怎么被加载器加载到JVM虚拟机的呢?
类加载有五步:加载, 验证, 准备, 解析, 初始化. 那么这五步都是干什么的呢?我们来看一下
我们的类在哪里呢? 在磁盘里(比如: target文件夹下的class文件), 我们先要将class类加载到内存中. 加载到内存区域以后, 不是简简单单的转换成二进制字节码文件,他会经过一系列的过程. 比如: 验证, 准备, 解析, 初始化等. 把这一些列的信息转变成内元信息, 放到内存里面去. 我们来看看具体的过程
![](https://img-blog.csdnimg.cn/img_convert/13591c8a04aac2df0494b32f89fb7f65.png)
第一步: 加载.
将class类加载到java虚拟机的内存里去, 在加载到内存之前, 会有一系列的操作。第一步是验证字节码。
![](https://img-blog.csdnimg.cn/img_convert/e2686a60c226d8991a6f8c4138e81caa.png)
第二步:验证
验证字节码加载是否正确, 比如:打开一个字节码文件。打眼一看, 感觉像是乱码, 实际上不是的. 其实,这里面每个字符串都有对应的含义. 那么文件里面的内容我们能不能替换呢?当然不能, 一旦替换, 就不能执行成功了. 所以, 第一步:验证, 验证什么呢?
验证字节码加载是否正确: 格式是否正确. 内容是否符合java虚拟机的规范.
第三步:准备
验证完了, 接下来是准备. 准备干什么呢? 比如我们的类Math, 他首先会给Math里的静态变量赋值一个初始值. 比如我们Math里有两个静态变量
publicstaticintinitData=666;
publicstaticUseruser=newUser();
在准备的过程中, 就会给这两个变量赋初始值, 这个初始值并不是真实的值, 比如initData的初始值是0. 如果是boolean类型, 就赋值为false. 也就是说, 准备阶段赋的值是jvm固定的, 不是我们定义的值.如果一个final的常量, 比如public static final int name="zhangsan", 那么他在初始化的时候, 是直接赋初始值"zhangsan"的. 这里只是给静态变量赋初始值
第四步:解析
接下来说说解析的过程. 解析的过程略微复杂, 解析是将"符号引用"转变为直接引用.
什么是符号引用呢?
比如我们的程序中的main方法. 写法是固定的, 我们就可以将main当成一个符号. 比如上面的initData, int, static, 我们都可以将其称之为符号, java虚拟机内部有个专业名词,把他叫做符号. 这些符号被加载到内存里都会对应一个地址. 将"符号引用"转变为直接引用, 指的就是, 将main, initData, int等这些符号转变为对应的内存地址. 这个地址就是代码的直接引用. 根据直接引用的值,我们就可以知道代码在什么位置.然后拿到代码去真正的运行.
将符号引用转变为"内存地址", 这种有一个专业名词, 叫静态链接. 上面的解析过程就相当于静态链接的过程. 类加载期间,完成了符号到内存地址的转换. 有静态链接, 那么与之对应的还有动态链接.
什么是动态链接呢?
publicstaticvoidmain(String[] args) {
Math math = newMath();
math.compute();
}
比如:上面这段代码, 只有当我运行到math.compute()这句话的时候, 才回去加载compute()这个方法. 也就是说, 在加载的时候, 我不一定会把compute()这个方法解析成内存地址. 只有当运行到这行代买的时候, 才会解析.
我们来看看汇编代码
javap -v Math.class
Classfile/Users/luoxiaoli/Downloads/workspace/project-all/target/classes/com/lxl/jvm/Math.classLastmodified2020-6-27;size777bytesMD5checksuma6834302dc2bf4e93011df4c0b774158Compiledfrom"Math.java"publicclasscom.lxl.jvm.Mathminor version:0major version:52flags:ACC_PUBLIC,ACC_SUPERConstant pool:#1 = Methodref #9.#35 // java/lang/Object."<init>":()V#2 = Class #36 // com/lxl/jvm/Math#3 = Methodref #2.#35 // com/lxl/jvm/Math."<init>":()V#4 = Methodref #2.#37 // com/lxl/jvm/Math.compute:()I#5 = Fieldref #2.#38 // com/lxl/jvm/Math.initData:I#6 = Class #39 // com/lxl/jvm/User#7 = Methodref #6.#35 // com/lxl/jvm/User."<init>":()V#8 = Fieldref #2.#40 // com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;#9 = Class #41 // java/lang/Object#10 = Utf8 initData#11 = Utf8 I#12 = Utf8 user#13 = Utf8 Lcom/lxl/jvm/User;#14 = Utf8 <init>#15 = Utf8 ()V#16 = Utf8 Code#17 = Utf8 LineNumberTable#18 = Utf8 LocalVariableTable#19 = Utf8 this#20 = Utf8 Lcom/lxl/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 MethodParameters#32 = Utf8 <clinit>#33 = Utf8 SourceFile#34 = Utf8 Math.java#35 = NameAndType #14:#15 // "<init>":()V#36 = Utf8 com/lxl/jvm/Math#37 = NameAndType #21:#22 // compute:()I#38 = NameAndType #10:#11 // initData:I#39 = Utf8 com/lxl/jvm/User#40 = NameAndType #12:#13 // user:Lcom/lxl/jvm/User;#41 = Utf8 java/lang/Object
{
publicstaticintinitData;descriptor:Iflags:ACC_PUBLIC, ACC_STATICpublicstaticcom.lxl.jvm.Useruser;descriptor:Lcom/lxl/jvm/User;flags:ACC_PUBLIC, ACC_STATICpubliccom.lxl.jvm.Math();descriptor:()Vflags:ACC_PUBLICCode:stack=1, locals=1, args_size=10:aload_01:invokespecial#1 // Method java/lang/Object."<init>":()V4:returnLineNumberTable:line 3:0LocalVariableTable:StartLengthSlotNameSignature050thisLcom/lxl/jvm/Math;publicintcompute();descriptor:()Iflags:ACC_PUBLICCode:stack=2, locals=4, args_size=10:iconst_11:istore_12:iconst_23:istore_24:iload_15:iload_26:iadd7:bipush109:imul10:istore_311:iload_312:ireturnLineNumberTable:line 8:0line 9:2line 10:4line 11:11LocalVariableTable:StartLengthSlotNameSignature0130thisLcom/lxl/jvm/Math;2111aI492bI1123cIpublicstaticvoidmain(java.lang.String[]);descriptor:([Ljava/lang/String;)Vflags:ACC_PUBLIC, ACC_STATICCode:stack=2, locals=2, args_size=10:new#2 // class com/lxl/jvm/Math3:dup4:invokespecial#3 // Method "<init>":()V7:astore_18:aload_19:invokevirtual#4 // Method compute:()I12:pop13:returnLineNumberTable:line 15:0line 16:8line 17:13LocalVariableTable:StartLengthSlotNameSignature0140args [Ljava/lang/String;861mathLcom/lxl/jvm/Math;MethodParameters:NameFlagsargsstatic {};descriptor:()Vflags:ACC_STATICCode:stack=2, locals=0, args_size=00:sipush6663:putstatic#5 // Field initData:I6:new#6 // class com/lxl/jvm/User9:dup10:invokespecial#7 // Method com/lxl/jvm/User."<init>":()V13:putstatic#8 // Field user:Lcom/lxl/jvm/User;16:returnLineNumberTable:line 4:0line 5:6
}
SourceFile:"Math.java"
使用这个指令, 就可以查看Math的二进制文件. 其实这个文件,就是上面那个二进制代码文件.
看看这里面有什么东西?
类的名称, 大小,修改时间, 大版本,小版本, 访问修饰符等等
Last modified 2020-6-27; size 777bytes
MD5 checksum a6834302dc2bf4e93011df4c0b774158
Compiled from"Math.java"
public classcom.lxl.jvm.Math
minor version: 0
major version: 52
还有一个Constant pool 常量池. 这个常量池里面有很多东西. 我们重点看中间哪一行. 第一列表示一个常量的标志符, 这个标识符可能在其他地方会用到. 第二列就表示常量内容.
Constant pool:
#1 = Methodref #9.#35// java/lang/Object."<init>":()V#2 = Class#36// com/lxl/jvm/Math#3 = Methodref #2.#35// com/lxl/jvm/Math."<init>":()V#4 = Methodref #2.#37// com/lxl/jvm/Math.compute:()I#5 = Fieldref #2.#38// com/lxl/jvm/Math.initData:I#6 = Class#39// com/lxl/jvm/User#7 = Methodref #6.#35// com/lxl/jvm/User."<init>":()V#8 = Fieldref #2.#40// com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;#9 = Class#41// java/lang/Object#10 = Utf8 initData
#11 = Utf8 I
#12 = Utf8 user
#13 = Utf8 Lcom/lxl/jvm/User;
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/lxl/jvm/Math;
#21 = Utf8 compute
这些标识符在后面都会被用到, 比如main方法
publicstaticvoidmain(java.lang.String[]);descriptor:([Ljava/lang/String;)Vflags:ACC_PUBLIC,ACC_STATICCode:stack=2,locals=2,args_size=10:new#2 // class com/lxl/jvm/Math3:dup4:invokespecial#3 // Method "<init>":()V7:astore_18:aload_19:invokevirtual#4 // Method compute:()I12:pop13:returnLineNumberTable:line 15:0line 16:8line 17:13LocalVariableTable:StartLengthSlotNameSignature0140args [Ljava/lang/String;861mathLcom/lxl/jvm/Math;MethodParameters:NameFlagsargs
这里面就用到了#2 #3 #4 ,这都是标识符的引用.
第一句: new了一个Math(). 我们看看汇编怎么写的?
0: new #2// class com/lxl/jvm/Math
new + #2. #2是什么呢? 去常量池里看, #2代表的就是Math类
#2 = Class#36// com/lxl/jvm/Math
这里要说的还是math.compute()这个方法, 不是在类加载的时候就被加载到内存中去了, 而是运行main方法的时候, 执行到这行代码才被加载进去, 这个过程叫做动态链接.
类加载的时候, 我们可以把"解析"理解为静态加载的过程. 一般像静态方法(例如main方法), 获取其他不变的静态方法会被直接加载到内存中, 因为考虑到性能, 他们加载完以后就不会变了, 就直接将其转变为在内存中的代码位置.
而像math.compute()方法, 在加载过程中可能会变的方法(比如compute是个多态,有多个实现), 那么在初始化加载的时候, 我们不会到他会调用谁, 只有到运行时才能知道代码的实现, 所以在运行的时候在动态的去查询他在内存中的位置, 这个过程就是动态加载
第五步: 初始化
对类的静态变量初始化为指定的值. 执行静态代码块. 比如代码
publicstaticint initData = 666;
在准备阶段将其赋值为0, 而在初始化阶段, 会将其赋值为设定的666
1.3 类的懒加载
类被加载到方法区中以后,主要包含:运行时常量池, 类型信息, 字段信息, 方法信息, 类加载器的引用, 对应class实例的引用等信息.
什么意思呢? 就是说, 当一个类被加载到内存, 这个类的常量,有常量名, 类型, 域信息等; 方法有方法名, 返回值类型, 参数类型, 方法作用域等符号信息都会被加载放入不同的区域.
注意: 如果主类在运行中用到其他类,会逐步加载这些类, 也就是说懒加载. 用到的时候才加载.
package com.lxl.jvm;
publicclassTestDynamicLoad {
static {
System.out.println("********Dynamic load class**************");
}
publicstaticvoidmain(String[] args) {
new A();
System.out.println("*********load test*****************");
B b = null; // 这里的b不会被加载, 除非new B();
}
}
classA {
static {
System.out.println("********load A**************");
}
publicA(){
System.out.println("********initial A**************");
}
}
classB {
static {
System.out.println("********load B**************");
}
publicB(){
System.out.println("********initial B**************");
}
}
这里定义了两个类A和B, 当使用到哪一个的时候, 那个类才会被加载, 比如:main方法中, B没有被用到, 所以, 他不会被加载到内存中.
运行结果
********Dynamic load class**************
********load A**********************initial A**************
*********load test*****************
我们看到A类被加载了,而B类没有被加载,原因是B类只声明了,没有用到。
总结几点如下:
静态代码块在构造方法之前执行
没有被真正使用的类不会被加载
二. 类加载器
2.1 类加载器的类型
类主要通过类加载器来加载, java里面有如下几种类加载器
1. 引导类加载器(Bootstrap ClassLoader)
在上面类加载流程中,说到在 [启动虚拟机的过程中, 会创建一个引导类加载器的实例] 这个引导类加载器的目的是什么呢?加载类
引导类加载器主要负责加载最最核心的java类型。 这些类库位于jre目录的lib目录下**. 比如:rt.jar, charset.jar等,
2. 扩展类加载器(Ext ClassLoader)
扩展类加载器主要是用来加载扩展的jar包。 加载jar的目录位于jre目录的lib/ext扩展目录中的jar包
3. 应用程序类加载器(App CloassLoader)
主要是用来加载用户自己写的类的。 负责加载classPath路径下的类包
4. 自定义类加载器
负责加载用户自定义路径下的类包
引导类加载器是由C++帮我们实现的, 然后c++语言会通过一个Launcher类将扩展类加载器(ExtClassLoader)和应用程序类加载器(AppClassLoader)构造出来, 并且把他们之间的关系构建好.
2.2 案例
案例一:测试jdk自带的类加载器
package com.lxl.jvm;
import sun.misc.Launcher;
import java.net.URL;
publicclassTestJDKClassLoader {
publicstaticvoidmain(String[] args) {
/**
* 第一个: String 是jdk自身自带的类,位于jre/lib核心目录下, 所以, 他的类加载器是引导类加载器
* 第二个: 加密类的classloader, 这是jdk扩展包的一个类
* 第三个: 是我们当前自己定义的类, 会被应用类加载器加载
*/
System.out.println(String.class.getClassLoader()); System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
}
}
我们来看这个简单的代码, 运行结果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
解析:
第一个: String 是jdk自身自带的类, 所以, 他的类加载器是引导类加载器,引导类加载器是c++代码,所以这里返回null
第二个: 加密类的classloader, 这是jdk扩展包的一个类, jdk扩展包里面使用的是extClassLoader类加载器加载的
第三个: 是我们当前自己定义的类, 会被AppClassLoader应用程序加载器加载.
我们看到ExtClassLoader和AppClassLoader都是Launcher类的一部分. 那Launcher类是什么东西呢?
上面有提到, Launcher类是jvm启动的时候由C++调用启动的一个类. 这个类引导加载器加载并创建其他的类加载器。
那么,第一个bootstrap引导类加载器, 那引导类加载器返回的为什么是null呢?
因为bootstrap引导类加载器, 他不是java的对象, 他是c++生成的对象, 所以这里是看不到的
案例二: BootstrapClassLoad和ExtClassLoader、AppClassLoader的关系
![](https://img-blog.csdnimg.cn/img_convert/b4b62d1d71074aa7705c587c00d5fe48.png)
如上图,左边是C语言程序代码实现, 右边是java代码实现。这里是跨语言调用,JNI实现了有c++向java跨语言调用。c语言调用的第一个java类是Launcher类。
从这个图中我们可以看出,C++调用java创建JVM启动器, 其中一个启动器是Launcher, 他实际是调用了sun.misc.Launcher类的getLauncher()方法. 那我们就从这个方法入手看看到底是如何运行的?
我们看到Lanucher.java类是在核心的rt.jar包里的,Lanucher是非常核心的一个类。
我们看到getLauncher()类直接返回了launcher. 而launcher是一个静态对象变量, 这是一个单例模式
C++调用了getLauncher()-->直接返回了lanucher对象, 而launcher对象是在构建类的时候就已经初始化好了. 那么,初始化的时候做了哪些操作呢?接下来看看他的构造方法.
![](https://img-blog.csdnimg.cn/img_convert/4b389f22cba36dd82aefa7e9c7d53e2c.png)
在构造方法里, 首先定义了一个ExtClassLoader. 这是一个扩展类加载器, 扩展类加载器调用的是getExtClassLoader(). 接下来看一看getExtClassLoader这个方法做了什么?
![](https://img-blog.csdnimg.cn/img_convert/7832f8ad2d8fe65d181665efa8134eb9.png)
这是一个典型的多线程同步的写法。
在这里, 判断当前对象是否初始化过, 如果没有, 那么就创建一个ExtClassLoader()对象, 看看createExtClassLoader()这个方法做了什么事呢?
![](https://img-blog.csdnimg.cn/img_convert/3fa762340cd1bf24ff29452eb17bee08.png)
doPrivileged是一个权限校验的操作, 我们可以先不用管, 直接看最后一句, return new Launcher.ExtClassLoader(var1). 直接new了一个ExtClassLoader, 其中参数是var1, 代表的是ext扩展目录下的文件.
![](https://img-blog.csdnimg.cn/img_convert/dc8f2b22ba709b30f08403e7a52f21f7.png)
在ExtClassLoader(File[] var1)这个方法中, 这里第一步就是调用了父类的super构造方法. 而ExtClassLoader继承了谁呢? 我们可以看到他继承了URLClassLoader.
![](https://img-blog.csdnimg.cn/img_convert/2d29b695d11083bf334493e8f047882c.png)
而URLClassLoader是干什么用的呢? 其实联想一下大概能够猜数来, 这里有一些文件路径, 通过文件路径加载class类.
我们继续看调用的super(parent), 我们继续往下走, 就会看到调用了ClassLoader接口的构造方法:
![](https://img-blog.csdnimg.cn/img_convert/dadc8f6d8d2b5d08a965f21e4547ae47.png)
这里设置了ExtClassLoader的parent是谁? 注意看,我们发现, ExtClassLoader的parent类是null.
![](https://img-blog.csdnimg.cn/img_convert/bb5836e5a605e30828170802192d2d33.png)
这就是传递过来的parent类加载器, 那么这里的parent类加载器为什么是null呢? 因为, ExtClassLoader的父类加载器是谁呢? 他是Bootstrap ClassLoader. 而BootStrap ClassLoader是C++的类加载器, 我们不能直接调用它, 所以, 设置为null.
其实, ExtClassLoader在初始化阶段就是调用了ExtClassLoader方法, 初始化了ExtClassLoader类
接下来,我们回到Launcher的构造方法, 看看Launcher接下来又做了什么?
![](https://img-blog.csdnimg.cn/img_convert/311a78e48a615b98f97b42cd969486e1.png)
可以看到, 接下来调了AppClassLoader的getAppClassLoader(var1), 这个方法. 需要注意一下的是var1这个参数. var1是谁呢? 向上看, 可以看到var1是ExtClassLoader.
![](https://img-blog.csdnimg.cn/img_convert/24da2cb80a4951a308b3669c17545939.png)
这是AppClassLoader, 应用程序类加载器, 这个类是加载我们自己定义的类的类加载器. 他也是继承自URLClassLoader.
我们来看看getAppClassLoader(final ClassLoader var0)方法. 这个方法的参数就是上面传递过来的ExtClassLoader
这里第一句话就是获取当前项目的class 文件路径, 然后将其转换为URL. 并调用了Launcher.AppClassLoader(var1x, var0), 其中var1x是class类所在的路径集合, var0是扩展的类加载器ExtClassLoader, 接下来, 我们进入到这个方法里看一看
![](https://img-blog.csdnimg.cn/img_convert/d0e646f9f973421c6df1b7f364b74203.png)
AppClassLoader直接调用了其父类的构造方法, 参数是class类路径集合, 和ExtClassLoader
![](https://img-blog.csdnimg.cn/img_convert/8e7fdd4d9c07f087d81cfa50637bd3a4.png)
![](https://img-blog.csdnimg.cn/img_convert/b479f85b2045010996c66d166ac61290.png)
最后, 我们看到, 将ExtClassLoader传递给了parent变量. 这是定义在ClassLoader中的属性, 而ClassLoader类是所有类加载器的父类. 因此, 我们也可以看到AppClassLoader的父类加载器是ExtClassLoader
同时, 我们也看到了, C++在启动JVM的时候, 调用了Launcher启动类, 这个启动类同时加载了ExtClassLoader和AppClassLoader.
publicstaticvoidmain(String[] args) {
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapClassLoad = extClassLoader.getParent();
System.out.println("bootstrap class loader: " + bootstrapClassLoad);
System.out.println("ext class loader " + extClassLoader);
System.out.println("app class loader "+ appClassLoader);
}
通过这个demo, 我们也可以看出, appClassLoader的父类是extClassLoader, extClassLoader的父类是bootstrapClassLoader
输出结果:
bootstrap classloader: null
ext classloader sun.misc.Launcher$ExtClassLoader@2a84aee7
app classloader sun.misc.Launcher$AppClassLoader@18b4aac2
通过上面的源码分析,我们发现引导类加载器创建并加载了扩展类加载器和应用类加载器。而扩展类加载器的父加载器是引导类加载器。应用类加载器的父加载器是扩展类加载器。这个结构,决定了后面类的加载方式,也就是双亲委派机制。