前置说明:由于个人能力有限,下面文章会大量整理、引用其他人的文章,我个人主要把这篇文章当成是自己的学习笔记
通过前面的文章,我们知道了,一段java代码是如何运行的?
1、【编译】程序员编写的java文件(编译成)class文件
2、【加载】JVM通过类加载器进行加载该class文件
3、【解释】class文件(通过JVM的执行引擎翻译成)机器码
4、【执行指令】机器码(通过特定平台的操作系统)运行
接下来我们要讲的就是其中的第二步:类加载子系统
类加载:JVM通过类加载器进行加载class文件
首先,我们需要了解下class文件
一、Class文件
1、class文件的二进制形式(了解即可)
JVM—加载到方法区的Class文件长什么样?——在这篇文章你可看见class文件的二进制形式
【推荐阅读】:Java虚拟机:class类文件结构
class类文件的结构:
- 魔数
- 文件版本信息
- 常量池
- 访问标志
- 类索引、父类索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
2、使用javap工具反汇编查看class文件
【推荐阅读】java常用工具
正常来说,我们不会直接看这种二进制,我们可以使用javap工具帮助阅读
javap -c +class文件(在class文件所在的目录下或者写class文件的全路径)
-c 对代码进行反汇编
-p 将打印私有的字段和方法;
-v 即verbose 输出附加信息
如何使用javap工具?
1、使用javac xxxx.java // 编译出class文件
2、使用javap -c -p -v +class文件 //反汇编查看class文件
java原文件
public class test003_helloworld {
final private String name ="张三";
public static void main(String[] args) {
int a =1;
int b =2;
int c;
c =a+b;
System.out.println(c);
System.out.println("hello world");
System.out.println("hello world2");
}}
反汇编后的内容:
Classfile /D:/IDEAProjects/000Users/ZKF/HomeWork/src/com/zkf/JVM/003Heap/test003_helloworld.class
Last modified 2022年9月30日; size 566 bytes
SHA-256 checksum c8bbe0b4ff6cbdff4e3090a13a95cc0501e7a1af28d32ad07538d57a84d0cc14
Compiled from "test003_helloworld.java"
public class test003_helloworld
minor version: 0
major version: 59
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #10 // test003_helloworld
super_class: #2 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // 寮犱笁
#8 = Utf8 寮犱笁
#9 = Fieldref #10.#11 // test003_helloworld.name:Ljava/lang/String;
#10 = Class #12 // test003_helloworld
#11 = NameAndType #13:#14 // name:Ljava/lang/String;
#12 = Utf8 test003_helloworld
#13 = Utf8 name
#14 = Utf8 Ljava/lang/String;
#15 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#16 = Class #18 // java/lang/System
#17 = NameAndType #19:#20 // out:Ljava/io/PrintStream;
#18 = Utf8 java/lang/System
#19 = Utf8 out
#20 = Utf8 Ljava/io/PrintStream;
#21 = String #22 // hello world
#22 = Utf8 hello world
#23 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#24 = Class #26 // java/io/PrintStream
#25 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
#29 = String #30 // hello world2
#30 = Utf8 hello world2
#31 = Utf8 ConstantValue
#32 = Utf8 Code
#33 = Utf8 LineNumberTable
#34 = Utf8 main
#35 = Utf8 ([Ljava/lang/String;)V
#36 = Utf8 SourceFile
#37 = Utf8 test003_helloworld.java
{
private final java.lang.String name;
descriptor: Ljava/lang/String;
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
ConstantValue: String 寮犱笁
public test003_helloworld();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #7 // String 寮犱笁
7: putfield #9 // Field name:Ljava/lang/String;
10: return
LineNumberTable:
line 1: 0
line 2: 4
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
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: istore_3
8: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3
12: invokevirtual #21 // Method java/io/PrintStream.println:(I)V
15: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #27 // String hello world
20: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #32 // String hello world2
28: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: return
LineNumberTable:
line 5: 0
line 6: 2
line 8: 4
line 9: 8
line 10: 15
line 11: 23
line 12: 31
}
SourceFile: "test003_helloworld.java"
javap返汇编后的内容 一般包括以下四个部分
2.1、基本信息
Classfile /D:/workspace/donkey/cimc-im-desktop/target/classes/com/brilliantstar/cimc/im/desktop/Demo.class
Last modified 2018-9-6; size 909 bytes//上次修改时间、大小909字节
MD5 checksum 0f9cd841c4a2ab1f6a00163e55ca6e0f//MD5算法
Compiled from "Demo.java"//编译自Demo.java文件
public class com.brilliantstar.cimc.im.desktop.Demo
minor version: 0//java小版本
major version: 52//java大版本(52对应jdk8版本)
flags: ACC_PUBLIC, ACC_SUPER//类的修饰符
附:md5是什么:
md5将整个文件当作一个大文本信息,通过其不可逆的字符串变换算法,产生了这个唯一的md5信息摘要。
md5就可以为任何文件(不管其大小、格式、数量)产生一个同样独一无二的“数字指纹”,如果任何人对文件做了任何改动,其md5值也就是对应的“数字指纹”都会发生变化
2.2 常量池(用来存放各种常量及符号引用)
常量池中的每一项都有一个对应的索引(如 #1),并且可能引用其他的常量池项(#1 = Methodref #6.#21)
Methodref:方法引用 后面两个引用表示【方法所属类】和【调用的方法名】
Fieldref:表示属性引用 【所属类】和【成员变量名】
NameAndType:
【方法名】: <init>表示构造方法,
【参数类型、返回值类型】: ()V 无参,V表示返回类型Void
描述符:描述方法的参数类型以及返回值类型
如上述例子:<( [ Ljava / lang / String;)V>
说明参数类型是Ljava / lang / String;返回值类型是V,即为 void 类型
同理:其他标识字符如下
2.3 字段区域,用来列举该类中的各个字段
这里最主要的信息便是该【字段的类型(descriptor: I)】以及【访问权限(flags: (0x0002) ACC_PRIVATE)】
对于声明为 final 的静态字段而言,如果它是基本类型或者字符串类型,那么字段区域还将包括它的常量值
2.4 方法区域(用来列举该类中的各个方法)
每个方法中包含以下5类信息
- 1、方法描述符和访问权限
- 2、每个方法还包括最为重要的代码区域(Code:)
Code代码区域一开始会声明该方法中的
2.1、操作数栈最大深度stack:2
(操作数栈主要用于:保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间)
(说明这里为什么是2?a+b 同时操作a+b)
如果是System.out.println(a+b);那么stack为3,因为还要操作一个System.out引用
2.2、局部变量最大槽数locals:4
3定义了abc3个变量 加上0索引位置的this引用
(局部变量表从1开始计数 , 并不是没有第 0个元素 , 第0个元素是当前类 this , 这是所有的局部变量表固定的格式)
注意这里局部变量指的是字节码中的局部变量,而非 Java 程序中的局部变量
2.3、该方法接收参数的个数(args_size=1)
附:行数表和局部变量表均属于调试信息。Java 虚拟机并不要求 class 文件必备这些信息
3、推荐jclasslib插件
jclasslib 插件安装及使用
使用这款插件可以很方便的查看class的字节码指令
附图:
二、类加载
部分内容引用:搞懂java类加载机制和类加载器
上面讲完了class文件的结构后,同时,我们知道程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过类加载器进行类加载该class文件到内存,完成该类进行初始化
类的生命周期(7个阶段):【加载、连接(验证、准备、解析)、初始化】、使用、卸载
(验证、准备、解析三个阶段统称为连接)
(类加载的全过程为前面的5个阶段)
1、加载阶段
定义:加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并【在堆中生成一个代表这个类的java.lang.Class对象】,作为方法区类数据的访问入口,【这个过程需要类加载器参与】
加载阶段需要完成3件事:
1、(通过一个类的全限定名来)【获取】定义此【类的二进制字节流】
2、将这个字节流所代表的【静态存储结构转化方法区的运行时数据结构】(将类的字节码载入方法中,内部采用c++的instanceKlass描述java类),它的重要field有:
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
3、在【堆内存中生成】一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口
(该Class对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口)
思考:【java类信息存储在方法区中】,类的对象在堆中,那么对象是如何获取类信息的呢?
1、类在JVM中是用【InstanceKlass】的实例表示,用来描述【保存】【Java类的信息】并存在元空间(metaSpace) /即方法区
2、每个对象的对象头中都保存了指向类的指针,通过该指针找到Class对象的地址(该Class对象保存了_java_mirror的地址,而_java_mirror保存了instanceKlass地址)
附:深入讲解Java的对象头与对象组成
总结:
1、加载类信息到方法区(元空间)
2、生成Class对象(堆中)
3、目的(堆中有了Class对象后能做什么?):
堆中对象能获取方法区中的类信息
(过程)堆对象–Class对象–(方法区中)instanceKlass ,获取类信息
2、连接阶段
定义:当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)类连接又可分为如下3个阶段
2.1、验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理.
验证阶段大致上分为四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证
2.2、准备:正式为类变量(static变量)分配内存空间并设置类变量初始值(默认值) 的阶段,这些内存都将在方法区中进行分配
备注:类变量不包括实例变量
附加说明:
- static变量在JDK7之前存储于instanceKlass末尾,从JDK 7开始,存储于_java_mirror末尾
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,【赋值在初始化阶段完成】
- 赋值的两个例外:
如果static变量是final的基本类型(以及字符串常量final static String a= “hello”),那么【编译阶段】值就确定了,赋值在准备阶段完成(final static int a = 20;因为常量必须赋值)
- 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
2.3、解析:虚拟机常量池的符号引用替换为直接引用过程
符号引用:符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要运行时能定位到目标即可
直接引用:
直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关。
直接引用的目标一定已经加载到了虚拟机的内存中了
(虚拟机可以根据需要自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它)
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型
3、初始化
执行类的类构造器<clinit>方法(为变量完成赋值)
(前面的几个类加载的动作,除了在加载阶段用户可以通过自定义类加载器的方式局部参与外,其余动作都完全由java虚拟机来主导控制【直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序】)
如何判断类进行了初始化?
类的初始化就是执行cinit方法,这个方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句【合并产生】的,可以用静态代码块是否执行为依据来判断类是否初始化了
不会导致类初始化的情况
- 访问类的static final静态常量(基本类型和字符串)不会触发初始化
(编译阶段)
备注:statis final Integer c =20;算是final修饰引用类型 ,会触发类的初始化(因为Integer.valueOf(20))
- 类对象.class不会触发初始化(1、加载阶段)
- 创建该类的数组不会触发初始化(同上,也是只加载了类)
- 类加载器的loadClass方法(1、加载阶段)
Class.forName的参数2为false时(该参数表示不进行类的初始化)
那什么时候进行初始化呢?概括得说,类初始化是【懒惰的】
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 【子类初始化】,如果父类还没初始化, 会引发(因为子类初始化要使用父类的东西)
- 子类【访问父类的静态变量】,只会触发父类的初始化
- Class.forName
- new会导致初始化(执行构造方法)
4、各个阶段的小总结
加载:在堆中生成Class对象,作为方法区类数据的访问入口
验证:验证字节码的文件的正确性(4种验证)文件格式的验证,字节码验证,符号引用的验证
准备:在方法区中为【类变量】分配内存空间并设置类中变量的【初始值】
解析:把类里面引用的其他类也加载进来,把符号引用转变为直接引用,也叫静态链接。
初始化:给静态变量一些真正的值,执行静态代码块。
使用:执行引擎
卸载:异常终止,操作系统错误,程序结束
三、类加载器
【推荐阅读】:JVM–三大子系统详解
1、类加载器是什么?
JVM设计团推有意把“通过一个类的全限定名来获取描述该类的二进制字节流”(加载阶段)这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)
2、类加载器的分类
启动类加载器(Bootstrap ClassLoader):负责加载java_HOME/lib目录中的类库,启动程序。
扩展类加载器(Extension ClassLoader):负责加载java_HOME/lib/ext目录中的类库,外部库。
应用程序类加载器 (Application ClassLoader):负责加载用户自己写的类库
名称 |
加载哪些类 |
说明 |
Bootstrap ClassLoader |
JAVA_HOME/jre/lib |
无法直接访问 (getClassLoader显示为null) |
Extension ClassLoader |
JAVA_HOME/jre/libl/ext |
上级为Bootstrap (getparent显示为null) |
Application ClassLoader |
classpath |
上级为Extension |
自定义类加载器 |
自定义 |
上级为Application |
3、双亲委派模型类加载机制
JVM通过双亲委派机制对类进行加载。双亲委派机制是指一个类在收到类加载的请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载器中不存在),则父类将会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被加载成功,若是找不到该类,则JVM会抛出ClassNotFoud异常。
具体流程如下所示:原文链接:https://blog.csdn.net/Artisan_w/article/details/105365314
双亲委派模型的好处:双亲委派模式可以保证Java程序的稳定运行,防止重复加载和任意修改
比如java.lang.Object,无论哪个类加载器需要加载这个类,最终都由Bootstrap ClassLoader加载,因此Object在程序的各个类加载器环境都是同一个类。相反,如果如果不用双亲委派模型进行加载,用户自定义了一个Object类并放置在类路径下,最终可能会引发程序混乱
双亲委派模型很好地解决了基础类的统一问题,保证了虚拟机的安全性
双亲委派模型的缺陷:
有些接口是Java核心库提供的,而java核心库是由启动类加载器进行加载,而这些接口的实现却是来自于不同的厂商提供的jar包,java的启动类加载器是不会加载这些jar包,这样传统的双亲委托模式就无法满足SPI的要求。
(SPI机制是JDK提供接口,第三方Jar包实现该接口)
https://zhuanlan.zhihu.com/p/185612299
举例: 接口:java.sql.Driver,定义在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类,
很明显java.sql包是由BootstrapClassloader加载器加载的;而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的
问题:【当前类加载器为启动类加载器】,要加载子类的mysql数据库驱动(com.mysql.jdbc.Driver类)【需要的类加载器为:应用类加载器】,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖
上面的问题中,我们需要一个工具,来帮助我们从启动类加载器 改变成 应用类加载器,这个工具就是线程上下文类加载器
如何解决?
类似这种,接口由启动类加载器加载,但是由于实现类由第三方实现,不在JDK中,无法加载实现类,都需要反向委派,由线程上下文加载器加载第三方实现类,【通过当前线程设置上下文类加载器】设置的上下文类加载器来【实现对于接口实现类的加载】
4、线程上下文类加载器
线程上下文加载器之所以打破双亲委派模型是因为:
【双亲委派模型依赖的单一方向的,并不能解决父类加载器去依赖子类加载器这种逆方向需求】,此时,需要线程上下文类加载器修改当前类加载器
如何加载?
1、获取现在类加载器(为了获取线程上下文加载器)
2、获取线程上下文加载器
3、自定义方法
备注:如果没有设置线程上下文类加载器,该加载器默认为应用类加载器
//1、获取现在类加载器(为了获取线程上下文加载器)
//2、获取线程上下文加载器
ClassLoader calssLoader = Thread.currentThread().getContextClassLoader();
try {
//设置线程上下文类加载器为自定义的加载器
Thread.currentThread.setContextClassLoader(targetTccl);
myMethod(); //执行自定义的方法
} finally {
//还原线程上下文类加载器
Thread.currentThread().setContextClassLoader(classLoader);
}
线程上下文类加载器的适用场景:
- 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
-
当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
引用自:https://www.cnblogs.com/jelly12345/p/15668489.html
简而言之就是ContextClassLoader
默认存放了AppClassLoader
的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()
取出应用程序类加载器来完成需要的操作
附:启动器类加载器追加路径
可以将自定义的类手动添加到Bootstrap类加载器的范围内
-Xbootclasspath表示设置bootclasspath
/a:.表示将【当前目录】追加至bootclasspath之后
启动类加载器的核心类可以修改和替换
- java -Xbootclasspath: //设置新路径
- java -Xbootclasspath/a:<追加路径> //后追加路径
java -Xbootclasspath/p:<追加路径> //前追加路径,一般不用,是给JVM底层开发的人用的