本文是《深入理解Java虚拟机(第二版)》第七章的笔记
类加载的时机
类加载后要初始化。所以可以通过判断啥时候要初始化,得出类加载的时机。
有且只有5种情况需要对类进行初始化
new
实例化对象、读取或设置静态字段(被final修饰放入常量池时除外)、调用类的静态方法 (且类没有初始化)。- 使用
java.lang.reflect
包的方法对类进行反射调用,且类没有初始化 - 初始化一个类时,如果父类没有初始化,会触发父类的初始化
- 含main方法的类会优先初始化
- 当使用JDK1.7的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic、REF_pubStatic、REF_invokeStatic
的方法句柄,并且这个句柄对应的类没有初始化,会触发其初始化。(黑人问号。。)
被动引用的例子
- 用子类引用父类的静态字段
SubClass.value
(value在父类中),只会初始化父类。 - 通过数组来定义引用类,不会触发次类的初始化
final
修饰的静态常量会存入常量池,引用它不会触发初始化(可以与上面的第一种情况比较)
与接口初始化的比较
- 接口不能使用static{}语句块,但有
<clinit>()
类构造器 - 与第三条不同:初始化一个接口时,如果父类接口没有初始化,父类接口不会初始化。
类加载的过程
加载
在加载期间虚拟机完成以下三件事:
- 通过类的全限定名获取定义此类的二进制字节流
- 通过这个二进制流代表的静态存储结构转化为方法区的运行时数据
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口(HotSpot虚拟机中Class对象是 在方法区中)
验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
为类的变量(static修饰)分配内存并设置变量的初始值。这些变量所用的内存都在方法区中分配。
注意:这里的初始值是其默认值。如果是static final
修饰,就初始化为指定值,存在常量区。
解析
将虚拟机中的符号引用替换为直接引用
主要的解析动作
- 类或者接口的解析
- 字段解析
- 类方法的解析
- 接口方法的解析
初始化
执行<clinit>()
方法的过程。和<init>()
比较下:
<clinit>()
:类的初始化,类变量的赋值动作和静态语句块合并一起。<init>()
:类的实例化,也就是类的构造方法。初始化实例用的。
一些细节:
- 定义在静态语句块后面的变量,静态语句块可以赋值但不能访问。如果访问会报错“非法前向引用”。
public class Clinit { |
- 优先执行父类的
<clinit>()
方法 - 接口的父类
<clinit>()
方法不需要先执行,并且接口的实现类在初始化时也不会执行接口的<clinit>()
方法。 - 如果多个线程同时初始化一个类,只有一个线程会执行类的
<clinit>()
方法。
类加载器
类加载器干的事:通过类的全限定名获取定义此类的二进制字节流。只有被同一个类加载器所加载的类才有可能相等。
优势:类层次划分、OSGi、热部署、代码加密 等等。
类加载器分类
- 启动类加载器:c++实现,是虚拟机等一部分,加载
<JAVA_HOME>/lib
目录下的类 - 扩展类加载器:加载
<JAVA_HOME>/lib/ext
目录下的类 - 应用程序类加载器:加载用户路径上指定的类库
- 自定义类加载器:用户自定义的类加载器
双亲委派模型
- 要求:除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
- 过程:就是把加载的活推拖给父类加载器,一直推到最顶层。然后让最顶层加载器加载,如果它干不了,再递给子,直到推给发起者,如果它也干不了,就发出
ClassNotFoundException
异常。 - 优点:使类有类层次性。如在双亲委派模型下,Object类是由最顶层的启动类加载器加载,所以它在每种加载器中都是同一个类,使Object这一最基础的类的性能得以保证。