一、类文件结构
JVM 可以理解的代码就叫做字节码
,即 .class文件。java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
Class文件由ClassFile
定义,ClassFile
的结构如下:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
接下来对几个重要属性做出解释。
1.魔数(Magic Number)
u4 magic; //Class 文件的标志
每个 Class 文件的头 4 个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。
2.版本号
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
高版本的 java 虚拟机可以执行低版本编译器生成的 Class 文件,反之不行。
3.常量池
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
常量池主要存放两大常量:字面量和符号引用。每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
4.访问标识
这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public
或者 abstract
类型,如果是类的话是否声明为 final
等等。
5. 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
类索引
用于确定这个类的全限定名,父类索引
用于确定这个类的父类的全限定名。
接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合中。
6.字段表集合
字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
7.方法表集合
u2 methods_count;//Class 文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
8.属性表集合
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
二、类加载过程
1.类的生命周期
2.类的加载过程
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
(1)加载
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass()
方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
加载阶段主要完成以下三件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
(2)验证
(3)准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区
中分配。
- 进行内存分配的仅包括类变量(静态变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
- 这里所设置的初始值**“通常情况”**下是数据类型默认的零值(如 0、0L、null、false 等),特殊情况:比如给 value 变量加上了 final 关键字
public static final int value=111
,那么准备阶段 value 的值就被赋值为 111。
(4)解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
(5)初始化
此时的初始化也就是真正意义上的赋予我们定义的初始值。
编译后会自动生成用来初始化的方法<clinit> ()
, <clinit> ()
方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。
一般只有在使用类时才会对类进行初始化,但是以下六种情况,必须对类进行初始化!
**A-**当遇到 new
、 getstatic
、putstatic
或 invokestatic
这 4 条直接码指令时,比如 new
一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
-
当 jvm 执行
new
指令时会初始化类。即当程序创建一个类的实例对象。 -
当 jvm 执行
getstatic
指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 -
当 jvm 执行
putstatic
指令时会初始化类。即程序给类的静态变量赋值。 -
当 jvm 执行
invokestatic
指令时会初始化类。即程序调用类的静态方法。**B-**使用
java.lang.reflect
包的方法对类进行反射调用时如Class.forname("...")
,newInstance()
等等。如果类没初始化,需要触发其初始化。
**C-**初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
**D-**当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main
方法的那个类),虚拟机会先初始化这个类。
E-MethodHandle
和 VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle
来初始化要调用的类。
**F-**当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
3.类的卸载
类的卸载需要满足三个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用。
- 该类的类加载器的实例已被 GC。
jvm 自带的类加载器加载的类是不会被卸载的,但是由我们自定义的类加载器加载的类是可能被卸载的。
jdk 自带的
BootstrapClassLoader
,ExtClassLoader
,AppClassLoader
负责加载 jdk 提供的类,所以它们(类加载器的实例)不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
三、类加载器
1.类加载器总结
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类
2.双亲委派模型
每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
3.双亲委派模型的好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
4.如何打破双亲委派模型
自定义加载器的话,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。