目录
前提: 运行环境
1. 类加载的过程
1.1 类加载器初始化的过程
1.2 类加载的过程
1.3 类的懒加载
2. jvm核心类加载器
3. 双亲委派机制
4. 自定义类加载器
5. tomcat类加载机制
运行环境:
我是在mac上操作的. 先找到mac的java地址. 从~/.bash_profile中可以看到
java的home目录是: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home
一. 类加载的过程
1.1 类加载器初始化的过程
假如现在有一个java类 com.lxl.jvm.Math类,里面有一个main方法,
package com.lxl.jvm; public class Math { static int initData = 666; static User user = new User(); int compute() { int a = 1; int b = 2int c = (a + b) * 10return c; } void main(String[] args) { Math math = Math(); math.compute(); } }
这个方法很简单,通常我们直接执行main方法就ok,可以运行程序了,那么点击运行main方法,整个过程是如何被加载的呢? 为什么点击执行main,就能得到结果呢?
先来看看答题的类加载流程,如下图:
备注:
1. windows上的java启动程序是java.exe,mac下,就是java
2. c语言部分,我们做了解,java部门是需要掌握的部分.
第一步: java调用底层的jvm.dll文件创建java虚拟机(这一步由C++实现) . 这里java是c++写的代码,调用的jvm.dll也是c++底层的一个函数. 通过调用jvm.dll文件,会创建java虚拟机. java虚拟机的启动都是c++程序实现的. dll文件相当于java的jar包. 开启虚拟机以后,会做哪些操作呢? 看第二步
第二步:在启动虚拟机的过程中,会创建一个引导类加载器的实例. 这个引导类的加载器是C语言实现的. 然后jvm虚拟机就启动起来了. 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类加载到内存中. 加载到内存区域以后,不是简简单单的转换成二进制字节码文件,他会经过一系列的过程. 比如: 验证,初始化等. 把这一些列的信息转变成内元信息,放到内存里面去. 我们来看看具体的过程
第一步: 加载. 将class类加载到java虚拟机的内存里去,在加载到内存之前,会有一些列的操作,第一个是验证字节码,比如:打开一个字节码文件
打眼一看,感觉像是乱码,实际上不是的. 其实,这里面每个字符串都有对应的含义. 那么文件里面的内容我们能不能替换呢?当然不能,一旦替换,就不能执行成功了. 所以,第一步:验证,验证什么呢?
验证字节码加载是否正确: 格式是否正确. 验证,验证的就是里面的内容,是否符合java虚拟机的规范. 验证完了,接下来是准备. 准备干什么呢? 比如我们的类Math,他首先会给Math里的静态变量赋值一个初始值. 比如我们Math里有两个静态遍历
; new User();
在准备的过程中,就会给这两个变量赋初始值,这个初始值并不是真实的值,比如initData的初始值是0. 如果是boolean类型,就赋值为false. 也就是说,准备阶段赋的值是jvm固定的,不是我们定义的值.如果一个final的常量,比如public static final int name="zhangsan",那么他在初始化的时候,是直接赋初始值"zhangsan"的. 这里只是给静态变量赋初始值
记下来说说解析的过程. 解析的过程略微复杂,解析是将"符号引用"转变为直接引用.
什么是符号引用呢?
比如我们的程序中的main方法. 写法是固定的,我们就可以将main当成一个符号. 比如上面的initData,int,static,我们都可以将其称之为符号,java虚拟机内部有个专业名词,把他叫做符号. 这些符号被加载到内存里都会对应一个地址. 将"符号引用"转变为直接引用,指的就是,将main,initData,int等这些符号转变为对应的内存地址. 这个地址就是代码的直接引用. 根据直接引用的值,我们就可以知道代码在什么位置.然后拿到代码去真正的运行.
将符号引用转变为"内存地址",这种有一个专业名词,叫静态链接. 上面的解析过程就相当于静态链接的过程. 类加载期间,完成了符号到内存地址的转换. 有静态链接,那么与之对应的还有动态链接.
什么是动态链接呢?
Math();
math.compute();
}
比如:上面这段代码,只有当我运行到math.compute()这句话的时候,才回去加载compute()这个方法. 也就是说,在加载的时候,我不一定会把compute()这个方法解析成内存地址. 只有当运行到这行代买的时候,才会解析.
我们来看看汇编代码
javap -v Math.class
Classfile /Users/luoxiaoli/Downloads/workspace/project-all/target/classes/com/lxl/jvm/Math. Last modified 2020-6-27; size 777 bytes MD5 checksum a6834302dc2bf4e93011df4c0b774158 Compiled from "Math.java" com.lxl.jvm.Math minor version: 0 major version: 52 flags: ACC_PUBLIC,ACC_SUPER Constant pool: #1 = Methodref #9.#35 // java/lang/Object."<init>":()V #2 = Class #36 com/lxl/jvm/Math #3 = Methodref #2.# com/lxl/jvm/Math."<init>":()V #4 = Methodref #37 com/lxl/jvm/Math.compute:()I #5 = Fieldref #38 com/lxl/jvm/Math.initData:I #6 = Class #39 com/lxl/jvm/User #7 = Methodref #6.# com/lxl/jvm/User."<init>":()V #8 = Fieldref #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 this20 = 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 { initData; descriptor: I flags: ACC_PUBLIC,ACC_STATIC static com.lxl.jvm.User user; descriptor: Lcom/lxl/jvm/User; flags: ACC_PUBLIC,1)">public com.lxl.jvm.Math(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1,locals=1 : aload_0 1: invokespecial #1 Method java/lang/Object."<init>":()V 4: LineNumberTable: line 3: LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lxl/jvm/Math; compute(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2,1)">4,1)">: iconst_1 : istore_1 : iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul : istore_3 11: iload_3 12: ireturn LineNumberTable: line 8: line 9: 10: 11: 0 13 Math; 2 11 a I 4 9 b I 11 2 c I main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC,ACC_STATIC Code: stack=0: new #2 class com/lxl/jvm/Math : dup 4: invokespecial #3 Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 Method compute:()I : pop 13: 15: 16: 17: 1314 0 args [Ljava/lang/String; 8 6 1 math Lcom/lxl/jvm/Math; MethodParameters: Name Flags args {}; descriptor: ()V flags: ACC_STATIC Code: stack=0,1)">0 0: sipush 666 3: putstatic #5 Field initData:I 6: 6 class com/lxl/jvm/User : dup 10: invokespecial #7 Method com/lxl/jvm/User."<init>":()V 13: putstatic #8 Field user:Lcom/lxl/jvm/User; 16: 4: 5: } SourceFile: "
使用这个指令,就可以查看Math的二进制文件. 其实这个文件,就是上面那个二进制代码文件.
看看这里面有什么东西?
Last modified 52
还有一个Constant pool 常量池. 这个常量池里面有很多东西. 我们重点看中间哪一行. 第一列表示一个常量的标志符,这个标识符可能在其他地方会用到. 第二列就表示常量内容.
Constant pool: #21 = Utf8 compute
这些标识符在后面都会被用到,比如main方法
new #2 4: invokespecial #3 9: invokevirtual #4 Math; MethodParameters: Name Flags args
这里面就用到了#2 #3 #4,这都是标识符的引用.
第一句: new了一个Math(). 我们看看汇编怎么写的?
class com/lxl/jvm/Math
new + #2. #2是什么呢? 去常量池里看,#2代表的就是Math类
# com/lxl/jvm/Math
这里要说的还是math.compute()这个方法,不是在类加载的时候就被加载到内存中去了,而是运行main方法的时候,执行到这行代码才被加载进去,这个过程叫做动态链接.
类加载的时候,我们可以把"解析"理解为静态加载的过程. 一般像静态方法(例如main方法),获取其他不变的静态方法会被直接加载到内存中,因为考虑到性能,他们加载完以后就不会变了,就直接将其转变为在内存中的代码位置.
而像math.compute()方法,在加载过程中可能会变的方法(比如compute是个多态,有多个实现),那么在初始化加载的时候,我们不会到他会调用谁,只有到运行时才能知道代码的实现,所以在运行的时候在动态的去查询他在内存中的位置,这个过程就是动态加载
下一个步骤: 初始化: 对类的静态变量初始化为指定的值. 执行静态代码块. 比如代码
666;
在准备阶段将其赋值为0,而在初始化阶段,会将其赋值为设定的666
2. 验证
3. 准备
4. 解析
5. 初始化
1.3 类的懒加载
类被加载到方法区中以后,主要包含:运行时常量池,类型信息,字段信息,方法信息,类加载器的引用,对应class实例的引用等信息.
什么意思呢? 就是说,当一个类被加载到内存,这个类的常量,有常量名,类型,域信息等; 方法有方法名,返回值类型,参数类型,方法作用域等符号信息都会被加载放入不同的区域.
注意: 如果主类在运行中用到其他类,会逐步加载这些类,也就是说懒加载. 用到的时候才加载.
TestDynamicLoad { { System.out.println(********Dynamic load class**************"); } public static void main(String[] args) { new A(); System.out.println("*********load test*****************"); B b = null; // 这里的b不会被加载,除非new B(); } } class A { ********load A**************); } A(){ System.********initial A**************); } } B { ********load B************** B(){ System.********initial B**************); } }
这里定义了两个类A和B,当使用到哪一个的时候,那个类才会被加载,比如:main方法中,B没有被用到,所以,他不会被加载到内存中.
运行结果
********Dynamic load class**************
********load A**************
********initial A**************
*********load test*****************
总结几点如下:
2. 没有被真正使用的类不会被加载
二. jvm核心类加载器
类主要通过类加载器来加载,java里面有如下几种类加载器
1. 引导类加载器:负责加载支撑JVM运行的,位于jre目录的lib目录下的核心类. 比如:rt.jar,charset.jar等,
2. 扩展类加载器: 负责加载支撑JVM运行的,位于jre目录的lib/ext扩展目录中的jar包
3. 应用程序类加载器: 负责加载classPath路径下的类包,主要加载自己写的类
引导类加载器是由C++帮我们实现的,然后c++语言会通过一个Launcher类将扩展类加载器(ExtClassLoader)和应用程序类加载器(AppClassLoader)构造出来,并且把他们之间的关系构建好.
例1:
package com.lxl.jvm; import sun.misc.Launcher; import java.net.URL; TestJDKClassLoader { main(String[] args) { /** * 第一个: String 是jdk自身自带的类,他的类加载器是引导类加载器 * 第二个: 加密类的classloader,这是jdk扩展包的一个类 * 第三个: 是我们当前自己定义的类,会被应用类加载器加载 */ System.out.println(String..getClassLoader()); System.out.println(com.sun.crypto.provider.DESKeyFactory..getClassLoader().getClass().getName()); System.out.println(TestJDKClassLoader..getClassLoader().getClass().getName()); } }
我们来看这个简单的代码,运行结果:
null sun.misc.Launcher$ExtClassLoader sun.misc.Launcher$AppClassLoader
第一个: String 是jdk自身自带的类,他的类加载器是引导类加载器 第二个: 加密类的classloader,这是jdk扩展包的一个类,jdk扩展包里面使用的是extClassLoader类加载器加载的 第三个: 是我们当前自己定义的类,会被AppClassLoader应用程序加载器加载.
我们看到ExtClassLoader和AppClassLoader都是Launcher类的一部分. 那Launcher类是什么东西呢?
上面有提到,Launcher类是jvm启动的时候由C++调用启动的一个类.
那么,第一个bootstrap引导类加载器,那引导类加载器返回的为什么是null呢?
因为bootstrap引导类加载器,他不是java的对象,他是c++生成的对象,所以这里是看不到的
例2: ExtClassLoader和AppClassLoader是怎么生成的?
从这个图中我们可以看出,C++调用java创建JVM启动器,其中一个启动器是Launcher,他实际是调用了sun.misc.Launcher的getLauncher()方法. 那我们就从这个方法入手看看到底是如何运行的?
我们看到Lanucher.java类是在核心的rt.jar包里的
我们看到getLauncher()类直接返回了launcher. 而launcher是一个静态对象变量,这是一个单例模式
C++通过getLauncher()-->直接返回了lanucher对象,而launcher对象是在构建类的时候就已经初始化好了. 那么,初始化的时候做了哪些操作呢?接下来看看他的构造方法.
在构造方法里,首先定义了一个ExtClassLoader. 这是一个扩展类加载器,扩展类加载器调用的是getExtClassLoader(). 接下来看一看getExtClassLoader这个方法做了什么?
在这里,判断当前对象是否初始化过,如果没有,那么就创建一个ExtClassLoader()对象,看看createExtClassLoader()这个方法做了什么事呢?
doPrivileged是一个权限校验的操作,我们可以先不用管,直接看最后一句,return new Launcher.ExtClassLoader(var1). 直接new了一个ExtClassLoader,其中参数是var1,代表的是ext扩展目录下的文件.
在ExtClassLoader(File[] var1)这个方法中, 这里第一步就是调用了父类的super构造方法. 而ExtClassLoader继承了谁呢? 我么你可以看到他继承了URLClassLoader.
而URLClassLoader是干什么用的呢? 其实联想一下大概能够猜数来,这里有一些文件路径,通过文件路径加载class类.
我们继续看调用的super(parent),我们继续往下走,就会看到一下内容
这里设置了ExtClassLoader的parent是谁? 注意看的话,我们发现,ExtClassLoader的parent类是null.
这就是传递过来的parent类加载器,那么这里的parent类加载器为什么是null呢? 因为,ExtClassLoader的父类加载器是谁呢? 他是Bootstrap ClassLoader. 而BootStrap ClassLoader是C++的类加载器,我们不能直接调用它,设置为null.
其实,ExtClassLoader在初始化阶段就是调用了ExtClassLoader方法,初始化了ExtClassLoader类
接下来,我们回到Launcher的构造方法,看看Launcher接下来又做了什么?
可以看到,接下来调了AppClassLoader的getAppClassLoader(var1),这个方法. 需要注意一下的是var1这个参数. var1是谁呢? 向上看,可以看到var1是ExtClassLoader.
这是AppClassLoader,应用程序类加载器,这个类是加载我们自己定义的类的类加载器. 他也是继承自URLClassLoader.
我们来看看getAppClassLoader(final ClassLoader var0)方法. 这个方法的参数就是上面传递过来的ExtClassLoader
这里第一句话就是获取当前项目的class 文件路径,然后将其转换为URL. 并调用了Launcher.AppClassLoader(var1x,var0),其中var1x是class类所在的路径集合,var0是扩展的类加载器ExtClassLoader,接下来,我们进入到这个方法里看一看
AppClassLoader直接调用了其父类的构造方法,参数是class类路径集合,和ExtClassLoader
最后,我们看到,将ExtClassLoader传递给了parent变量. 这是定义在ClassLoader中的属性,而ClassLoader类是所有类加载器的父类. 因此,我们也可以看到AppClassLoader的父类加载器是ExtClassLoader
同时,我们也看到了,C++在启动JVM的时候,调用了Launcher启动类,这个启动类同时加载了ExtClassLoader和AppClassLoader.
main(String[] args) { ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); ClassLoader extClassLoader = appClassLoader.getParent(); ClassLoader bootstrapClassLoad = extClassLoader.getParent(); System.bootstrap class loader: " + bootstrapClassLoad); System.ext class loader extClassLoader); System.app class loader "+ appClassLoader); }
通过这个demo,我们也可以看出,appClassLoader的父类是extClassLoader,extClassLoader的父类是bootstrapClassLoader
输出结果:
bootstrap class loader: ext loader sun.misc.Launcher$ExtClassLoader@2a84aee7 app class loader sun.misc.Launcher$AppClassLoader@18b4aac2
原文链接:/jvm/997183.html