
JVM总结-04_类加载和字节码
字节码文件结构概述
我们都知道,java在推出初期,其宣传语是:一次编译,处处运行,为了保证在多个不同平台的兼容性,我们需要多做一件事情,
-
通过编译器把java文件编译成class类字节码文件
-
jvm虚拟机识别并解释执行class类字节码文件的二进制指令
这边大概过一遍字节码文件的结构,详细解释参考其他资料。
class类文件本质上是一个8位字节为单位的二进制流文件,各个部位按照严格的顺序来组合排列,jvm按照特定的规则来校验和解释执行
class采用伪结构来存储数据,有两种类型:无符号数和表。

-
魔数:头四个字节,0xCAFEBABE,用来校验是否是一个class文件,很多文件格式通用手段(png.jpg等)
-
常量池:放变量的属性,类型,名称,方法的属性,类型,名称等
-
访问标志:访问类型,是类还是接口,是否为public,类型是否为final
-
类,父类,接口索引:确定类的继承关系
-
字段表属性:描述类或者接口中的字段的属性,比如是否public,是否final,是否static等
-
方法表属性:描述方法的属性,比如是否public,返回值,类型
-
属性表属性:某些场景特有信息,比如字段表中特殊的属性,方法表特殊的属性等
反编译字节码文件
javap options classes
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
头文件信息
开头的7行信息为头文件信息,描述
-
Class文件的所在位置
-
最后修改时间
-
文件大小
-
MD5值
-
编译自哪个文件
-
类全限定名
-
jdk次版本号,主版本号
访问标志
| 标志名称 | 标志值 | 含义 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 是否为Public类型 |
| ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
| ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义. |
| ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
| ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
| ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
| ACC_ENUM | 0x4000 | 标志这是一个枚举 |
常量池
constant pool,可以理解为资源仓库,一个工人在盖楼时需要用到各种工具,常量池类似于工人的工具箱,每次根据需要去工具箱拿需要的工具,当调用方法时,进入运行时常量池,根据符号引用去找到变量地址(个人理解)
字面量:字母上的常量,如String,final常量,确定的值,如我确定需要xx型号的扳手
符号引用:占位符,在类加载或者运行时才会解析其具体的地址,如我需要一个锤子,锤子的位置等我需要使用时才会找到
-
类全限定名
-
字段名称和描述符号
-
方法名称和描述符号
方法表集合
对类的方法的描述,以表的形式记录,记录操作数栈,局部变量需要的空间,参数个数等信息,这样当程序调用该方法,方法入栈时,可以确定好这个方法所需要的信息
类名
类名
类加载机制
类生命周期
- 加载
连接 --->
-
验证
-
准备
-
解析
5.初始化
6.卸载

其中,解析因为动态机制,可能会发生在初始化后,这些动作是交替执行的
加载
根据类全限定名,查找并加载类的二进制数据
-
根据全限定名查找到二进制字节流
-
将字节流的静态存储结构转化成方法区的运行时结构
-
在内存中生成一个Class类对象,来定义这个类的信息
验证
验证文件是否安全
-
文件格式验证,根据头信息验证文件格式合法性
-
元数据验证,验证类的元数据语义正确性
-
字节码验证,对数据流和控制流分析
-
符号引用验证,确保解析时的符号引用替换能够正常执行
准备
为类的静态变量分配内存,并赋初始值
不包括实例变量,实例变量在对象实例化的时候放在堆中。
静态变量存放在方法区(堆中的元空间)中,声明不赋值也会赋默认初始值。在类加载时初始化一次。
解析
把类中的符号引用替换为直接引用
-
符号引用:以一组符号来代表目标,只要能区分就行
-
直接引用:直接指向目标的具体指针
-
类/接口解析
-
字段解析
-
方法解析
-
接口方法解析
初始化
为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化
执行类构造器<cinit>()方法(编译器自动生成的方法)
cinit:集成类中的所有赋值操作和静态语句块,可以看做此方法执行才会初始化静态语句块和赋值操作。
如果类中没有静态语句块也没有赋值操作,就不会生成
步骤:
-
若该类没有被加载和连接,先加载并连接
-
若该类的父类没有被加载和连接,先加载连接父类
-
若该类有其他初始化语句,依次执行初始化语句
类初始化时机:当类主动使用的时候才会初始化
-
创建了类的实例,new
-
访问了某个类的静态变量或赋值
-
调用类的静态方法
-
反射
-
初始化某个类的子类,父类也会初始化
-
jvm启动时标明的启动类,直接用java.exec命令运行某个主类
使用
类访问方法区中的数据结构的接口,对象在堆区
卸载
-
执行了System.gc()
-
程序正常结束
-
程序遇到异常或错误导致终止
-
操作系统错误导致jvm进程终止
类加载器

顾名思义,用来加载类的东西,注意,比较不同类加载器对象之间的比较是不等的,false
启动类加载器
父类的加载器,无法被直接引用,java.开头的,所有的jdk\jre\lib包下的类都由它加载
扩展类加载器
jdk\jre\ext下包的加载器,javax.开头的,可以直接使用该加载器
应用程序类加载器
如果没有定义自己的应用程序类加载器,该加载器为默认类加载器,加载类路径classpath下的对象
自定义类加载器
用户自定义的类加载器,可做定制化
-
自动验证签名
-
动态创建类
-
特定场景获取类
Class.forName()和ClassLoader.loadClass()区别
-
Class.forName():把类的class文件加载到jvm中,执行类加载,对类解释,初始化静态变量和静态代码块。(解析并初始化)
-
ClassLoader.LoadClass():把类class文件加载到jvm中,只到连接的阶段,执行到解析,等到newInstance时才会初始化
-
Class.forName(name,initialize,loader):带参函数,也可控制是否加载static块,只有调用newInstance()方法才会实例化对象。
全盘负责
一个类加载器负责加载某个Class时,该Class依赖和引用的类也由它来负责加载,除非显示让其他类加载器负责
父类委托/双亲委派
先让父类尝试加载,父类加载器无法加载时才由子类加载。
类加载器在加载类的时候,首先会移交给自己的父类去处理,当父类处理不了的时候,才会交由子类来处理。
这是为了保证避免子类和父类之间创建重名类,因为是根据全限定名来加载类的,不同的类加载器之间的全限定名可能会重复。
缓存机制
加载过的Class类会被缓存,需要使用某个Class时会先去缓存找,不存在时才会执行类加载,所以修改了Class后需要重启JVM
jdk9破坏双亲委派
jdk9中引入了java的模块化,jdk9之前根据classpath来寻找依赖,之后可显式指明依赖的模块,所以天然具有扩展性,故ext类加载器也就莫得了。
换成了Plat form Class Loader 平台类加载器