类加载器 - 类的加载、连接与初始化
类的加载、连接与初始化
概述
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
- 类型:可以理解为一个class
- 加载:查找并加载类的二进制数据,最常见的情况是将已经编译完成的类的class文件从磁盘加载到内存中
- 连接:确定类型与类型之间的关系,对于字节码的相关处理
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值。但是在到达初始化之前,类变量都没有初始化为真正的初始值
- 解析:在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用转换为直接引用的过程
- 初始化:为类的静态变量赋予正确的初始值
- 使用:比如创建对象,调用类的方法等
- 卸载:类从内存中销毁
理解:public static int number = 666;
上面这段代码,在类加载的连接阶段,为对象number分配内存,并初始化为0;然后再初始化阶段在赋予正确的初始值:666
类的使用方式
Java程序对类的使用方式可分为两种
- 主动使用
- 创建类的实例
- 访问某个类或接口的静态变量,或者对静态变量赋值
- 调用类的静态方法
- 反射
- 初始化类的子类
- Java虚拟机启动时被标明为启动类的类(包含main方法)
- JDK1.7开始提供的动态语言支持(java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化)
- 被动使用
- 除了主动使用的七种情况之外,其他使用Java类的方法都被看作是对类的被动使用,都不会导致类的初始化
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
代码理解
示例一:类的加载连接和初始化过程
代码一
public class Test01 {
public static void main(String[] args) {
System.out.println(Child01.str);
}
}
class Father01 {
public static String str = "做一个好人!";
static {
System.out.println("Father01 static block");
}
}
class Child01 extends Father01 {
static {
System.out.println("Child01 static block");
}
}
运行结果做一个好人!
Father01 static block
做一个好人!
代码二
public class Test01 {
public static void main(String[] args) {
System.out.println(Child01.str2);
}
}
class Father01 {
public static String str = "做一个好人!";
static {
System.out.println("Father01 static block");
}
}
class Child01 extends Father01 {
public static String str2 = "做一个好人!";
static {
System.out.println("Child01 static block");
}
}
运行结果
Father01 static block
Child01 static block
做一个好人!
分析:
- 代码一中,我们通过子类调用父类中的str,这个str是在父类被定义的,对Father01主动使用,没有主动使用Child01,故Child01的静态代码块没有执行,父类的静态代码块被执行了。 -> 对于静态字段来说,只有直接定义了该字段的类才会被初始化。
- 代码二中,对Child01主动使用;根据主动使用的7种情况,调动类的子类时,其所有的父类都会被先初始化,所以Father01会被初始化。 -> 当一个类初始化时,要求其父类全部都已经初始化完毕了。
以上验证的是类的初始化情况,那么如何验证类的加载情况呢,可以通过在启动的时候配置虚拟机参数:-XX:+TraceClassLoading
查看
运行代码一,查看输出结果,可以看见控制台打印了very多的日志,第一个加载的是java.lang.Object
类(不管加载哪个类,他的父类一定是Object类),后面是加载的一系列jdk的类,他们都位于rt包下。往下查看,可以看见Loaded classloader.Child01
,说明即使没有初始化Child01,但是程序依然加载了Child01类。
[Opened /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/local/jdk1.8/jre/lib/rt.jar]
...
[Loaded java.lang.Void from /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded classloader.Father01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
[Loaded classloader.Child01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
Father01 static block
做一个好人!
[Loaded java.lang.Shutdown from /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/local/jdk1.8/jre/lib/rt.jar]
拓展:JVM参数介绍
因为前一章节使用了JVM参数,所以对其做一下简单的介绍
- 所有的JVM参数都是以
-XX:
开头的 - 如果形式是:
-XX:+<option>
,表示开启option选项 - 如果形式是:
-XX:-<option>
,表示关闭option选项 - 如果形式是:
-XX:<option>=<value>
,表示将option选项的值设置为value
示例二:常量的本质含义
public class Test02 {
public static void main(String[] args) {
System.out.println(Father02.str);
}
}
class Father02{
public static final String str = "做一个好人!";
static {
System.out.println("Father02 static block");
}
}
执行结果
做一个好人!
分析
可以看见,此段代码并没有初始化Father02类。这是因为final表示的是一个常量,在编译阶段常量就会被存入到调用这个常量的方法所在的类的常量池当中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。在本代码中,常量str会被存入到Test02的常量池中,之后Test02与Father02没有任何关系,甚至可以删除Father02的class文件。
我们反编译一下Test02类
Compiled from "Test02.java"
public class classloader.Test02 {
public classloader.Test02();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String 做一个好人!
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
第一块是Test02类的构造方法,第二块是我们要看的main方法。可以看见3: ldc #4 // String 做一个好人!
,此时这个值已经是确定无疑的做一个好人!
了,而不是Father02.str
,证实了上面说的在编译阶段常量就会被存入到调用这个常量的方法所在的类的常量池当中。
拓展:助记符
因前一章节涉及到了助记符,所以介绍下本章节涉及到的助记符及扩展
- ldc:表示将int、float或String类型的常量值常量池中推送至栈顶
- bipush:表示将单字节(-128 -至 127)的常量推送至栈顶
- sipush:表示将一个短整形(-32768 至 32767)的常量推送至栈顶
- iconst_1:表示将int类型的
1
推送至栈顶(这类助记符只有iconst_m1 - iconst_5七个)
示例三:编译期常量与运行期常量的区别
public class Test03 {
public static void main(String[] args) {
System.out.println(Father03.str);
}
}
class Father03 {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("Father03 static block");
}
}
运行结果
Father03 static block
a60c5db4-2673-4ffc-a9f0-2dbe53fae583
分析
本代码与示例二的区别在于str
的值是在运行时确认的,而不是编译时就确定好的,属于运行期常量,而不是编译期常量。当一个常量的值并非编译期间确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致这个类被初始化。
示例四:数组创建本质
代码一
public class Test04 {
public static void main(String[] args) {
Father04 father04_1 = new Father04();
System.out.println("-----------");
Father04 father04_2 = new Father04();
}
}
class Father04 {
static {
System.out.println("Father04 static block");
}
}
运行结果
Father04 static block
-----------
分析
- 创建类的实例时,会初始化类
- 所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
代码二
public class Test04 {
public static void main(String[] args) {
Father04[] father04s = new Father04[1];
System.out.println(father04s.getClass());
}
}
运行结果
class [Lclassloader.Father04;
分析
- 创建数组对象不再主动使用的7种情况内,属于被动使用,故不会初始化Father04
- 打印father04s的类型为
[Lclassloader.Father04
,这是虚拟机在运行期生成的。 -> 对于数组示例来说,其类型是有JVM在运行期动态生成的,表示为[Lclassloader.Father04
这种形式,动态生成的类型,其父类就是Object。 - 对于数组来说,JavaDoc经常将构成数组的元素为Component,实际上就是将数组降低一个维度后的类型
反编译一下:
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: anewarray #2 // class classloader/Father04
4: astore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: aload_1
9: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: return
- anewarray:表示创建一个引用类型(如类、接口、数组)的数组,并将其引用值值压入栈顶
- newarray:表示创建一个指定的原始类型(如int、float、char等)的数组,并将其引用值压入栈顶
示例五:接口的加载与初始化
代码一
public class Test05 {
public static void main(String[] args) {
System.out.println(Child05.j);
}
}
interface Father05 {
int i = 5;
}
interface Child05 extends Father05 {
int j = 6;
}
运行结果
6
分析
- 接口中定义的常量本身就是public、static、final的
- 结果显而易见,这时我们删除掉Father05.class文件和Child05.class文件,程序依然可以正常运行
- 接口中的常量本身就是final常量,会被加载到Test05的常量池中
- 此时,Father05和Child05都不会被加载
代码二
public class Test05 {
public static void main(String[] args) {
System.out.println(Child05.j);
}
}
interface Father05 {
int i = 5;
}
interface Child05 extends Father05 {
int j = new Random().nextInt(8);
}
运行结果
6
将Father05.class文件删除,运行结果
Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Father05
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at classloader.Test05.main(Test05.java:15)
Caused by: java.lang.ClassNotFoundException: classloader.Father05
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 13 more
分析
- 只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会加载初始化
代码三
public class Test05 {
public static void main(String[] args) {
System.out.println(Child05.j);
}
}
interface Father05 {
Thread thread = new Thread() {
{
System.out.println("Father05 code block");
}
};
}
class Child05 implements Father05 {
public static int j = 8;
}
运行结果
8
分析
- 在初始化一个类时,并不会先初始化他所实现的接口
代码四
public class Test05 {
public static void main(String[] args) {
System.out.println(Father05.thread);
}
}
interface GrandFather {
Thread thread = new Thread() {
{
System.out.println("GrandFather code block");
}
};
}
interface Father05 extends GrandFather{
Thread thread = new Thread() {
{
System.out.println("Father05 code block");
}
};
}
运行结果
Father05 code block
Thread[Thread-0,5,main]
分析
- 在初始化一个接口时,并不会先初始化他的父接口
示例六:类加载器准备阶段和初始化阶段
代码一
public class Test06 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("i:" + Singleton.i);
System.out.println("j:" + Singleton.j);
}
}
class Singleton {
public static int i;
public static int j = 0;
private static Singleton singleton = new Singleton();
private Singleton() {
i ++;
j ++;
}
public static Singleton getInstance() {
return singleton;
}
}
运行结果
i:1
j:1
分析
首先Singleton.getInstance();
进入Singleton
的getInstance
方法,getInstance
会返回Singleton
的实例,Singleton
的实例是new Singleton();
出来的,因此调用了自定义的私有构造方法。在调用构造方法之前,给静态变量赋值,i
默认赋值为0,j
显式的赋值为0,经过构造函数之后,值都为1。
代码二
public class Test06 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("i:" + Singleton.i);
System.out.println("j:" + Singleton.j);
}
}
class Singleton {
public static int i;
private static Singleton singleton = new Singleton();
private Singleton() {
i ++;
j ++;
}
public static int j = 0;
public static Singleton getInstance() {
return singleton;
}
}
运行结果
i:1
j:0
分析
程序主动使用了Singleton类,准备阶段对类的静态变量分配内存,赋予默认值,下面给出类在连接及初始化阶段常量的值的变化
- i : 0
- singleton:null
- j : 0
- getInstance:初始化
- i:0
- singleton:调用构造函数
- i:1
- j:1
- j:0【覆盖了之前的1】
故返回的值i为1,j为0
深入解析类的加载、连接与初始化
类的加载
- 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区的数据结构
- 加载.class文件的方式
- 从本地系统直接加载
- 通过网络下载.class文件
- 从zip等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源代码动态编译为.class文件
- 类的加载的最终产品是位于内存中的Class对象
- Calss对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
- 有两种类型的类加载器
- Java虚拟机自带的类加载器
- 根类加载器(Bootstrap):该加载器没有父加载器。他负责加载虚拟机的核心类库,如java.lang.*等。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,呀没有继承java.lang.CalssLoader类
- 扩展类加载器(Extension):父加载器为根加载器。从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类
- 系统(应用)类加载器(System):父加载器为扩展加载器。从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader类的子类
- 用户自定义的类加载器
- java.lang.ClassLoader的子类
- 用户可以定制类的加载方式
- Java虚拟机自带的类加载器
- 类加载器并不需要等到某个类被“首次使用”时再加载他
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载他,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用时才报告错误(LinkageError错误)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
类的连接
类被加载后,就进入连接阶段。连接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时环境中去
类的验证
类的验证的内容
- 类文件的结构检查
- 语义检查
- 字节码验证
- 二进制兼容性验证
类的准备
在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于下面的Sample类,在准备阶段,将为int类型的静态变量
i
分配4个字节的内存空间,并且赋默认值0;为long类型的静态变量j分配8个字节的内存空间,并赋予默认值0
public class Sample {
private static int i = 8;
private static long j = 8L;
......
}
类的初始化
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:
- 在静态变量的声明处初始化
- 在静态代码块中初始化
静态变量的声明语句,预计静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行他们
类的初始化步骤
- 假如这个类还没有被加载和连接诶,需要先进行加载和连接
- 假如类存在直接父类,并且这个父类还没有被初始化,需要先初始化直接父类
- 假如类中存在初始化语句,需要依次执行这些初始化语句
类的初始化时机
-
当Java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适用于接口
- 在初始化一个类时,并不会先初始化他所实现的接口
- 在初始化一个接口时,并不会先初始化他的父接口
因此,一个父接口并不会因为他的子接口或实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。代码参照代码理解-接口的初始化
-
只有当程序访问的静态变量或者静态方法确实在当前类或者当前接口中定义时,才认为是对类或接口的主动使用
-
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
拓展部分
类实例化
类的生命周期除了前文提到的加载、连接、初始化之外,还有类示例化,垃圾回收和对象终结
- 为新的对象分配内存
- 为实例变量赋予默认值
- 为实例变量赋予正确的初始值
- Java编译器为它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被称为
<init>
。针对源代码中每一个类的构造方法,Java编译器都产生一个<init>
方法
类的卸载
- 当一个类被加载、连接和初始化后,它的生命周期就开始了。当代表该类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,这个类在方法区内的数据也会被卸载,从而结束自己的生命周期
- 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
- 由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些类加载器,而这类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的;由用户自定义的类加载器所加载的类是可以被卸载的