类的加载过程详解
概述
在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
按照 Java 虚拟机规范,从 Class 文件到加载到内存中的类,到类卸载出内存位置,它的整个生命周期包括如下七个阶段:
其中,验证、准备、解析 3 个部分统称为链接(Linking)。
从程序中类的使用过程看:
Loading(加载)阶段
加载的理解
所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用。
反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则 JVM 在运行期也无法反射。
加载完成的操作
加载阶段,简言之,查找并加载类的二进制数据,生成 Class 的实例。
在加载类时,Java 虚拟机必须完成以下 3 件事情:
- 通过类的全名,获取类的二进制数据流(类的全名:包名 + 类名)
- 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
- 创建
java.lang.Class
类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
二进制流的获取方式
对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合 JVM 规范即可)
- 虚拟机可能通过文件系统读入一个 Class 后缀的文件 (最常见)
- 读入 jar、zip 等归档数据包,提取类文件
- 事先存放在数据库中的类的二进制数据
- 使用类似于 HTTP 之类的协议通过网络进行加载
- 在运行时生成一段 Class 的二进制信息等
在获取到类的二进制信息后,Java 虚拟机就会处理这些数据,并最终转为一个 java.lang.Class
的实例。
如果输入数据不是 ClassFile 的结构(如 Class 文件的魔数不是 ca fe ba be
),则会抛出 ClassFormatError
。
类模型与 Class 实例的位置
类模型的位置
加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(JDK 1.8 之前:永久代;JDK 1.8 之后:元空间))。
Class 实例的位置
类将 .class 文件加载至元空间后,会在堆中创建一个 java.lang.Class
对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象。
外部可以通过访问代表 Order 类的 Class 对象来获取 Order 的类数据结构。
再说明
Class 类的构造方法是私有的,只有 JVM 能够创建。
java.lang.Class
实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过 Class 类提供的接口,可以获得目标类所关联的 .class 文件中具体的数据结构:方法、字段等信息。
代码示例:
public class LoadingTest {
public static void main(String[] args) {
try {
Class clazz = Class.forName("java.lang.String");
// 获取当前运行时类声明的所有方法
Method[] ms = clazz.getDeclaredMethods();
for (Method m : ms) {
// 获取方法的修饰符
String mod = Modifier.toString(m.getModifiers());
System.out.print(mod + " ");
// 获取方法的返回值类型
String returnType = m.getReturnType().getSimpleName();
System.out.print(returnType + " ");
// 获取方法名
System.out.print(m.getName() + "(");
// 获取方法的参数列表
Class[] ps = m.getParameterTypes();
if (ps.length == 0) System.out.print(')');
for (int i = 0; i < ps.length; i++) {
char end = (i == ps.length - 1) ? ')' : ',';
// 获取参数的类型
System.out.print(ps[i].getSimpleName() + end);
}
System.out.println();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
public boolean equals(Object)
public String toString()
public int hashCode()
public int compareTo(String)
public volatile int compareTo(Object)
public int indexOf(String,int)
public int indexOf(String)
public int indexOf(int,int)
public int indexOf(int)
static int indexOf(char[],int,int,char[],int,int,int)
static int indexOf(char[],int,int,String,int)
public static String valueOf(int)
public static String valueOf(long)
public static String valueOf(float)
public static String valueOf(boolean)
public static String valueOf(char[])
public static String valueOf(char[],int,int)
public static String valueOf(Object)
public static String valueOf(char)
public static String valueOf(double)
public char charAt(int)
private static void checkBounds(byte[],int,int)
public int codePointAt(int)
public int codePointBefore(int)
public int codePointCount(int,int)
public int compareToIgnoreCase(String)
public String concat(String)
public boolean contains(CharSequence)
public boolean contentEquals(CharSequence)
public boolean contentEquals(StringBuffer)
public static String copyValueOf(char[])
public static String copyValueOf(char[],int,int)
public boolean endsWith(String)
public boolean equalsIgnoreCase(String)
public static transient String format(Locale,String,Object[])
public static transient String format(String,Object[])
public void getBytes(int,int,byte[],int)
public byte[] getBytes(Charset)
public byte[] getBytes(String)
public byte[] getBytes()
public void getChars(int,int,char[],int)
void getChars(char[],int)
private int indexOfSupplementary(int,int)
public native String intern()
public boolean isEmpty()
public static transient String join(CharSequence,CharSequence[])
public static String join(CharSequence,Iterable)
public int lastIndexOf(int)
public int lastIndexOf(String)
static int lastIndexOf(char[],int,int,String,int)
public int lastIndexOf(String,int)
public int lastIndexOf(int,int)
static int lastIndexOf(char[],int,int,char[],int,int,int)
private int lastIndexOfSupplementary(int,int)
public int length()
public boolean matches(String)
private boolean nonSyncContentEquals(AbstractStringBuilder)
public int offsetByCodePoints(int,int)
public boolean regionMatches(int,String,int,int)
public boolean regionMatches(boolean,int,String,int,int)
public String replace(char,char)
public String replace(CharSequence,CharSequence)
public String replaceAll(String,String)
public String replaceFirst(String,String)
public String[] split(String)
public String[] split(String,int)
public boolean startsWith(String,int)
public boolean startsWith(String)
public CharSequence subSequence(int,int)
public String substring(int)
public String substring(int,int)
public char[] toCharArray()
public String toLowerCase(Locale)
public String toLowerCase()
public String toUpperCase()
public String toUpperCase(Locale)
public String trim()
数组类的加载
创建数组类的情况稍微有些特殊,因为 数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称 A)的过程:
如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public。
Linking(链接)阶段
验证Verification
当类加载到系统后,就开始链接操作,验证是链接操作的第一步。
它的目的是保证加载的字节码是合法、合理并符合规范的。
验证的步骤比较复杂,实际要验证的项目也很繁多,大体上 Java 虚拟机需要做以下检查,如图所示:
整体说明
验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等。
- 其中 格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中
- 格式验证之外的验证操作将会在方法区中进行
链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查。
具体说明
格式验证:是否以魔数 0xCAFEBABE 开头,主版本和副版本号是否在当前 Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等
语义检查:Java 虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:
- 是否所有的类都有父类的存在(在 Java 里,除了 Object 外,其他类都应该有父类)
- 是否一些被定义为 final 的方法或者类被重写或继承了
- 非抽象类是否实现了所有抽象方法或者接口方法
- 是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;absract 情况下的方法,就不能是final 的了)
字节码验证:Java 虚拟机还会进行字节码验证,字节码验证也是 验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:
- 在字节码的执行过程中,是否会跳转到一条不存在的指令
- 函数的调用是否传递了正确类型的参数
- 变量的赋值是不是给了正确的数据类型等
栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是,100% 准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的
在前面 3 次检查中,已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的。
符号引用的验证:校验器还将进行符号引用的验证。Class 文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,虚拟机就会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出 NoClassDefFoundError
,如果一个方法无法被找到,则会抛出 NoSuchMethdError
此阶段在解析环节才会执行。
准备Preparation
准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值。
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。
Java 虚拟机为各类型变量默认的初始值如表所示:
类型 | 默认初始值 |
---|---|
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
char | u0000 |
boolean | false |
reference | null |
Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,故对应的,boolean 的默认值就是 false。
注意:
- 这里不包含基本数据类型的字段用 static final 修饰的情况,因为 final 在编译的时候就会分配了,准备阶段会显式赋值
- 注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中
- 在这个阶段不会像初始化阶段中那样会有初始化或者代码被执行
/**
*
* 基本数据类型:非 final 修饰的变量,在准备环节进行默认初始化赋值
* final 修饰以后,在准备环节直接进行显式赋值
*
* 拓展:如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节直接进行显式赋值
*/
public class LinkingTest {
private static long id; // 在准备环节进行默认初始化赋值
private static final int num = 1; // 在准备环节直接进行显式赋值
public static final String constStr = "CONST"; // 在准备环节直接进行显式赋值
public static final String constStr1 = new String("CONST"); // 在准备环节进行默认初始化赋值
}
解析Resolution
在准备阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用。
具体描述
符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存分布无关。比较容理解的就是在 Class 类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下 println()
方法被调用时,系统需要明确知道该方法的位置。
举例:输出操作 System.out.println()
对应的字节码:
invokevirtual #24
以方法为例,Java 虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。
小结
所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。
不过 Java 虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在 HotSpot VM 中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着 JVM 在执行完初始化之后再执行。
字符串的复习
最后,再来看一下 CONSTANT_String
的解析。由于字符串在程序开发中有着重要的作用,因此,读者有必要了解一下 String 在 Java 虚拟机中的处理。当在 Java 代码中直接使用字符串常量时,就会在类中出现 CONSTANT_String
,它表示字符串常量,并且会引用一个 CONSTANT_UTF8
的常量项。在 Java 虚拟机内部运行中的常量池,会维护一张字符串拘留表(intern),它会保存所有出现过的字符串常量,并且没有重复项。只要以 CONSTANT_String
形式出现的字符串也都会在这张表中。使用 String.intern()
方法可以得到一个字符串在拘留表中的引用,因为该表中没有重复项,所以任何字面相同的字符串的 String.intern()
方法返回总是相等的。
Initialization(初始化)阶段
初始化阶段,简言之,为类的静态变量赋予正确的初始值。
具体描述
类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行 Java 字节码。(即:到了初始化阶段,才真正开始执行类中定义的 Java 程序代码)。
初始化阶段的重要工作是执行类的初始化方法:()
和 ()
方法。
()
方法仅能由 Java 编译器生成并由 JVM 调用,程序开发者无法自定义一个同名的方法,更无法直接在 Java 程序中调用该方法,虽然该方法也是由字节码指令所组成,它是类静态成员的赋值语句以及 static 语句块合并产生的()
方法是初始化普通的变量,并非 static 静态变量
说明
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类总是在子类之前被调用,也就是说,父类的 static 块优先级高于子类
Java 编译器并不会为所有的类都产生
()
初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含
()
方法?
- 一个类中并没有声明任何的类变量,也没有静态代码块时
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
一个类中包含 static final 修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
代码示例 1
public class InitializationTest {
public static int id = 1;
public static int number;
static {
number = 2;
System.out.println("father static{}");
}
public static void main(String[] args) {
System.out.println(number);
}
}
输出结果:
father static{}
2
代码示例 2
public class InitializationTest1 {
// 场景 1:对应非静态的字段,不管是否进行了显式赋值,都不会生成 () 方法
public int num = 1;
// 场景 2:静态的字段,没有显式的赋值,不会生成 () 方法
public static int num1;
// 场景 3:比如对于声明为 static final 的基本数据类型的字段,不管是否进行了显式赋值,都不会生成 () 方法
public static final int num2 = 1;
}
static与final的搭配问题
/**
*
* 哪些场景下,Java 编译器就不会生成 () 方法
*/
public class InitializationTest1 {
// 场景 1:对应非静态的字段,不管是否进行了显式赋值,都不会生成 () 方法
public int num = 1;
// 场景 2:静态的字段,没有显式的赋值,不会生成 () 方法
public static int num1;
//场景 3:比如对于声明为 static final 的基本数据类型的字段,不管是否进行了显式赋值,都不会生成 () 方法
public static final int num2 = 1;
}
/**
*
* 说明:使用 static + final 修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
* 情况 1:在链接阶段的准备环节赋值
* 情况 2:在初始化阶段 () 中赋值
*
* 结论:
* 在链接阶段的准备环节赋值的情况:
* 1. 对于基本数据类型的字段来说,如果使用 static final 修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
* 2. 对于 String 来说,如果使用字面量的方式赋值,使用 static final 修饰的话,则显式赋值通常是在链接阶段的准备环节进行
*
* 在初始化阶段 () 中赋值的情况
* 排除上述的在准备环节赋值的情况之外的情况
*
* 最终结论:使用 static + final 修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或 String 类型的显式赋值,是在链接阶段的准备环节进行
*/
public class InitializationTest2 {
public static int a = 1; // 在初始化阶段 () 中赋值
public static final int INT_CONSTANT = 10; // 在链接阶段的准备环节赋值
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在初始化阶段 () 中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000); // 在初始化阶段 () 中赋值
public static final String s0 = "helloworld0"; // 在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1"); // 在初始化阶段 () 中赋值
}
clinit的线程安全性
对于 ()
方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。
虚拟机会保证一个类的 ()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 ()
方法,其他线程都需要阻塞等待,直到活动线程执行 ()
方法完毕。
正是因为函数 ()
带锁线程安全的,因此,如果一个在类的 ()
方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行 ()
方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.kele.java1.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.kele.java1.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}
public class StaticDeadLockMain extends Thread {
private char flag;
public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}
@Override
public void run() {
try {
Class.forName("com.kele.java1.Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}
public static void main(String[] args) throws InterruptedException {
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}
什么时候初始化普通变量?
普通变量要和类一起初始化( 方法里),也是在初始化阶段,初始化阶段如下:
- 首先,加载父类中的静态代码块和静态属性,按照静态代码块和静态属性在代码中从上到下出现的先后顺序加载
- 然后,加载子类中的静态代码块和静态属性,按照静态代码块和静态属性在代码中从上到下出现的先后顺序加载
- 其次,加载父类中的普通代码块和普通属性,按照普通代码块和普通属性在代码中从上到下出现的先后顺序加载
- 最后,加载子类中的普通代码块和普通属性,按照普通代码块和普通属性在代码中从上到下出现的先后顺序加载
主动使用 vs 被动使用
Java 程序对类的使用分为两种:主动使用和被动使用。
主动使用
Class 只有在必须要首次使用的时候才会被装载,Java 虚拟机不会无条件地装载 Class 类型。Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的「使用」,是指主动使用,主动使用只有下列几种情况:(即:如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成)。
invokestatic
指令getstatic
或者 putsttic
指令(对应访问变量、赋值变量操作)java.lang.reflect
包中的方法反射类的方法时。比如:Class.forname("cn.kbt.java.Test")
main()
方法的那个类),虚拟机会先初始化这个主类REF_getStatic
、REF_putStatic
、REF_invokeStatic
方法句柄对应的类)代码可以看视频讲解:www.bilibili.com/video/BV1PJ…
代码示例 1:针对步骤 1、2
/**
*
* 测试类的主动使用:意味着会调用类的 (),即执行了类的初始化阶段
*
* 1. 当创建一个类的实例时,比如使用 new 关键字,或者通过反射、克隆、反序列化。
* 2. 当调用类的静态方法时,即当使用了字节码 invokestatic 指令。
*/
public class ActiveUse1 {
//序列化的过程:
@Test
public void test1() {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("order.dat"));
oos.writeObject(new Order());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null)
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//反序列化的过程:(验证)
@Test
public void test2() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("order.dat"));
Order order = (Order) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null)
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void test3(){
Order.method();
}
}
class Order implements Serializable{
static {
System.out.println("Order类的初始化过程");
}
public static void method(){
System.out.println("Order method()....");
}
}
输出结果:
// test1()
Order类的初始化过程
// test2()
Order类的初始化过程
// test3()
Order类的初始化过程
Order method()....
代码可以看视频讲解:www.bilibili.com/video/BV1PJ…
代码示例 2:针对步骤 3
public class ActiveUse2 {
@Test
public void test1(){
System.out.println(User.num);
// System.out.println(User.num1);
// System.out.println(User.num2);
}
@Test
public void test2(){
System.out.println(CompareA.NUM1);
// System.out.println(CompareA.NUM2);
}
}
class User{
static{
System.out.println("User类的初始化过程");
}
public static int num = 1;
public static final int num1 = 1;
public static final int num2 = new Random().nextInt(10);
}
interface CompareA{
public static final Thread t = new Thread(){
{
System.out.println("CompareA的初始化");
}
};
public static final int NUM1 = 1;
public static final int NUM2 = new Random().nextInt(10);
}
输出结果:
// test1()
3
// test2()
9
代码可以看视频讲解:www.bilibili.com/video/BV1PJ…
代码示例 3:针对步骤 4、5
/**
* 4. 当使用 java.lang.reflect 包中的方法反射类的方法时。比如:Class.forName("com.kele.java.Test")
* 5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
* 针对 5,补充说明:
* 当 Java 虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
* 在初始化一个类时,并不会先初始化它所实现的接口
* 在初始化一个接口时,并不会先初始化它的父接口
* 因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,
* 才会导致该接口的初始化。
*/
public class ActiveUse3 {
static{
System.out.println("ActiveUse3的初始化过程");
}
@Test
public void test1() {
try {
Class clazz = Class.forName("com.kele.java1.Order");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Test
public void test2() {
System.out.println(Son.num);
}
@Test
public void test3(){
System.out.println(CompareC.NUM1);
}
}
class Father {
static {
System.out.println("Father类的初始化过程");
}
}
class Son extends Father implements CompareB{
static {
System.out.println("Son类的初始化过程");
}
public static int num = 1;
}
interface CompareB {
public static final Thread t = new Thread() {
{
System.out.println("CompareB的初始化");
}
};
public default void method1(){
System.out.println("你好!");
}
}
interface CompareC extends CompareB {
public static final Thread t = new Thread() {
{
System.out.println("CompareC的初始化");
}
};
public static final int NUM1 = new Random().nextInt();
}
输出结果:
// test1()
ActiveUse3的初始化过程
Order类的初始化过程
// test2()
ActiveUse3的初始化过程
Father类的初始化过程
CompareB的初始化
Son类的初始化过程
1
// test3()
ActiveUse3的初始化过程
CompareC的初始化
425764602
针对步骤 5,补充说明
当 Java 虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。
代码可以看视频讲解:www.bilibili.com/video/BV1PJ…
代码示例 4:针对步骤 6、7、8
/**
* 6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
* 7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
* 8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
* (涉及解析 REF_getStatic、REF_putStatic、REF_invokeStatic 方法句柄对应的类)
*/
public class ActiveUse3 {
static{
System.out.println("ActiveUse3的初始化过程");
}
@Test
public void test4() {
System.out.println(Son.num);
}
}
class Father {
static {
System.out.println("Father类的初始化过程");
}
}
class Son extends Father implements CompareB{
static {
System.out.println("Son类的初始化过程");
}
public static int num = 1;
}
interface CompareB {
public static final Thread t = new Thread() {
{
System.out.println("CompareB的初始化");
}
};
public default void method1(){
System.out.println("你好!");
}
}
interface CompareC extends CompareB {
public static final Thread t = new Thread() {
{
System.out.println("CompareC的初始化");
}
};
public static final int NUM1 = new Random().nextInt();
}
输出结果:
// test4()
ActiveUse3的初始化过程
Father类的初始化过程
CompareB的初始化
Son类的初始化过程
1
针对步骤 7,说明
JVM 启动的时候通过引导类加载器加载一个初始类。这个类在调用 public static void main(String[])
方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载、链接和初始化。
代码可以看视频讲解:www.bilibili.com/video/BV1PJ…
/**
* 7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
*/
public class ActiveUse3 {
static{
System.out.println("ActiveUse3的初始化过程");
}
public static void main(String[] args) {
System.out.println("hello");
}
}
class Father {
static {
System.out.println("Father类的初始化过程");
}
}
class Son extends Father implements CompareB{
static {
System.out.println("Son类的初始化过程");
}
public static int num = 1;
}
interface CompareB {
public static final Thread t = new Thread() {
{
System.out.println("CompareB的初始化");
}
};
public default void method1(){
System.out.println("你好!");
}
}
interface CompareC extends CompareB {
public static final Thread t = new Thread() {
{
System.out.println("CompareC的初始化");
}
};
public static final int NUM1 = new Random().nextInt();
}
输出结果:
ActiveUse3的初始化过程
hello
被动使用
除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。
也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。
loadClass()
方法加载一个类,并不是对类的主动使用,不会导致类的初始化如果针对代码,设置参数 -XX:+TraceClassLoading
,可以追踪类的加载信息并打印出来。
代码示例 1:针对步骤 1、2
/**
* 关于类的被动使用,即不会进行类的初始化操作,即不会调用 ()
*
* 1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
* 当通过子类引用父类的静态变量,不会导致子类初始化
* 2. 通过数组定义类引用,不会触发此类的初始化
*
* 说明:没有初始化的类,不意味着没有加载!
*/
public class PassiveUse1 {
@Test
public void test1(){
System.out.println(Child.num);
}
@Test
public void test2(){
Parent[] parents = new Parent[10];
// System.out.println(parents.getClass());
// System.out.println(parents.getClass().getSuperclass());
// parents[0] = new Parent();
// parents[1] = new Parent();
}
}
class Parent{
static{
System.out.println("Parent的初始化过程");
}
public static int num = 1;
}
class Child extends Parent{
static{
System.out.println("Child的初始化过程");
}
}
输出结果:
// test1()
Parent的初始化过程
1
//test2()
// 空,代表没有初始化
代码示例 2:针对步骤 3、4
/**
* 3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
* 4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
*/
public class PassiveUse2 {
@Test
public void test1(){
System.out.println(Person.NUM);
// System.out.println(Person.NUM1);
}
@Test
public void test2(){
System.out.println(SerialA.ID);
// System.out.println(SerialA.ID1);
}
@Test
public void test3(){
try {
Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.kele.java1.Person");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class Person{
static{
System.out.println("Person类的初始化");
}
public static final int NUM = 1;//在链接过程的准备环节就被赋值为1了。
public static final int NUM1 = new Random().nextInt(10);//此时的赋值操作需要在()中执行
}
interface SerialA{
public static final Thread t = new Thread() {
{
System.out.println("SerialA的初始化");
}
};
int ID = 1;
int ID1 = new Random().nextInt(10);//此时的赋值操作需要在()中执行
}
输出结果:
// test1()
1 // 如果输出 NUM1 则输出Person类的初始化 和 9
// test2()
1 // 如果输出 ID1 则输出Person类的初始化 和 8
// test3()
// 空,代表没有初始化
类的Using(使用)
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化 3 个类加载步骤。一旦一个类型成功经历过这 3 个步骤之后,便「万事俱备,只欠东风」,就等着开发者使用了。
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用 new 关键字为其创建对象实例。
类的Unloading(卸载)
类、类的加载器、类的实例之间的引用关系
在类加载器的内部实现中,用一个 Java 集合来存放所加载类的引用。另一方面,一个 Class 对象总是会引用它的类加载器,调用 Class 对象的 getClassLoader()
方法,就能获得它的类加载器。由此可见,代表某个类的 Class 实例与其类的加载器之间为双向关联关系。
一个类的实例总是引用代表这个类的 Class 对象。在 Object 类中定义了 getClass()
方法,这个方法返回代表对象所属类的 Class 对象的引用。此外,所有的 Java 类都有一个静态属性 Class,它引用代表这个类的 Class 对象。
类的生命周期
当 Sample 类被加载、链接和初始化后,它的生命周期就开始了。当代表 Sample 类的 Class 对象不再被引用,即不可触及时,Class 对象就会结束生命周期,Sample 类在方法区内的数据也会被卸载,从而结束 Sample 类的生命周期。
一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期。
具体例子
Loader1
变量和 objClass 变量间接应用代表 Sample 类的 Class 对象,而 objClass
变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为 null,此时 Sample 对象结束生命周期,MyClassLoader 对象结束生命周期,代表 Sample 类的 Class 对象也结束生命周期,Sample 类在方法区内的二进制数据 被卸载。
当再次有需要时,会检查 Sample 类的 Class 对象是否存在,如果存在会直接使用,不再重新加载;如果不存在 Sample 类会被重新加载,在 Java 虚拟机的堆区会生成一个新的代表 Sample 类的 Class 实例(可以通过哈希码查看是否是同一个实例)。
类的卸载
综合以上三点,一个已经加载的类型被卸载的几率很小,至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能。
回顾:方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量 和 不再使用的类型。
HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
判定一个常量是否「废弃」还是相对简单,而要判定一个类型是否属于「不再被使用的类」的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收。也就是 Java 堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGI、JSP 的重加载等,否则通常是很难达成的
- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是「被允许」,而并不是和对象一样,没有引用了就必然会回收。