0%

JVM(7)-虚拟机类加载机制

本文是《深入理解Java虚拟机(第二版)》第七章的笔记

类加载的时机

类加载后要初始化。所以可以通过判断啥时候要初始化,得出类加载的时机。

有且只有5种情况需要对类进行初始化

  1. new实例化对象、读取或设置静态字段(被final修饰放入常量池时除外)、调用类的静态方法 (且类没有初始化)。
  2. 使用java.lang.reflect包的方法对类进行反射调用,且类没有初始化
  3. 初始化一个类时,如果父类没有初始化,会触发父类的初始化
  4. 含main方法的类会优先初始化
  5. 当使用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 {
static {
i = 0;
// System.out.println(i); // 非法前向引用
}
static int i = 1;
}
  • 优先执行父类的<clinit>()方法
  • 接口的父类<clinit>()方法不需要先执行,并且接口的实现类在初始化时也不会执行接口的<clinit>()方法。
  • 如果多个线程同时初始化一个类,只有一个线程会执行类的<clinit>()方法。

类加载器

类加载器干的事:通过类的全限定名获取定义此类的二进制字节流。只有被同一个类加载器所加载的类才有可能相等。

优势:类层次划分、OSGi、热部署、代码加密 等等。

类加载器分类

  • 启动类加载器:c++实现,是虚拟机等一部分,加载<JAVA_HOME>/lib目录下的类
  • 扩展类加载器:加载<JAVA_HOME>/lib/ext目录下的类
  • 应用程序类加载器:加载用户路径上指定的类库
  • 自定义类加载器:用户自定义的类加载器

双亲委派模型

  • 要求:除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
  • 过程:就是把加载的活推拖给父类加载器,一直推到最顶层。然后让最顶层加载器加载,如果它干不了,再递给子,直到推给发起者,如果它也干不了,就发出 ClassNotFoundException异常。
  • 优点:使类有类层次性。如在双亲委派模型下,Object类是由最顶层的启动类加载器加载,所以它在每种加载器中都是同一个类,使Object这一最基础的类的性能得以保证。