一、内存结构
类加载子系统的职责是:加载class文件到内存中。
完整的内存结构如下:
二、类加载过程
类加载过程总体分为Loading(加载)、Linking(链接)、Initialization(初始化)三个环节,在Linking阶段又细分为Verification(验证)、Preparation(准备)、Resolution(解析)三个环节。
1. 加载(Loading)
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
补充:加载class文件的方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用
- 从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
2. 链接(Linking)
(1)验证(Verification)
- 验证字节码文件的字节流中包含信息符合虚拟机规范,以防止不正常的字节流危害虚拟机安全。所有能被JVM识别的字节码文件,它的有效起始都是CA FE BA BE,它是JVM识别的一个标识。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 验证错误会报VerifyError
我们使用BinaryViewer打开.class文件,发现他的开头都是 CAFE BABE。
(2)准备(Preparation)
- 为类变量(非final修饰的static变量)分配内存(在方法区),设置其初始值(零值)
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的引用类型,即new的对象,赋值在初始化阶段完成
- 实例变量不会被分配初始化,实例变量随对象被分配到Java堆中。
(3)解析(Resolution)(存疑)
- 将常量池内的符号应用转换为直接引用。
- 符号引用是一组来描述所引用的目标。直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。
3. 初始化(Initialization)
(1)初始化阶段就是执行类构造器方法<clinit>()的过程。此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
也就是说,没有类变量和静态代码块,就不会有<clinit>,比如下面这段代码:
在字节码文件中,一个类的方法会被解析到methods文件目录下,构造器对应目录下的<init>()方法, main()就对应main()。
我们使用idea的jclasslib插件查看method,发现只有类构造器<init>和main方法。
(2)构造器方法中指令按语句在源文件中出现的顺序执行。
结合如下代码进行理解:
我们在静态代码块中使用了类变量number,但是number的定义是在整个静态代码块之后,能这样做的原因是,在链接的准备阶段,我们已经为number分配了内存,并设置初始值为0.
但是不能在声明之前进行引用。
使用jclasslib查看<clinit>的代码,如下:
这串代码是与文件里语句的顺序对应起来的。
因此执行这段代码后,number的值应该是10,而不是20.
(3)若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
(4)虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。
以这段代码为例,尝试多线程加载一个类。
执行这段代码的结果是,永远会有一个线程无法结束,因为<clinit>只会有一次,也就是说类只能被加载一次。