Class文件的加载过程java
ClassLoader的工做模式数据结构
类的热加载函数
1 Class文件的装载流程布局
只有被java虚拟机装载的Class类型才能在程序中使用(注意装载和加载的区别)优化
1.1 类装载的条件日志
Class只有在必需要使用的时候才会被装载,Java虚拟机不会无条件的装载Class类型。Java虚拟机规定:一个类或者接口在初次使用时,必须进行初始化。这里的使用指的是主动使用,主动使用有如下几种状况:对象
除了以上状况属于主动使用外,其余状况均属于被动使用,被动使用不会引发类的初始化。blog
例1:主动使用接口
public class Parent{内存
static{
System.out.println("Parent init");
}
}
public class Child{
static{
System.out.println("Child init");
}
}
public class InitMain{
public static void main(String[] args){
Child c = new Child();
}
}
以上声明了3个类:Parent Child InitMain,Child类为Parent类的子类。若Parent被初始化,根据代码中的static块可知,将会打印"Parent init",若Child被初始化,则会打印"Child init"。执行InitMain,结果为:
Parent init
Child init
由此可知,系统首先装载Parent类,接着装载Child类。符合主动装载中的两个条件,使用new关键字建立类的实例会装载相关的类,以及在初始化子类时,必须先初始化父类。
例2 :被动装载
public class Parent{
static{
System.out.println("Parent init ");
}
public static int v = 100; //静态字段
}
public class Child extends Parent{
static{
System.out.println("Child init");
}
}
public class UserParent{
public static void main(String[] args){
System.out.println(Child.v);
}
}
Parent中有静态变量v,而且在UserParent中,使用其子类Child去调用父类中的变量。
运行代码:
Parent init
100
虽然在UserParent中,直接访问了子类对象,可是Child子类并未初始化,只有Parent父类进行初始化。因此,在引用一个字段时,只有直接定义该字段的类,才会被初始化。
注意:虽然Child类没有被初始化,可是,此时Child类已经被系统加载,只是没有进入初始化阶段。
能够使用-XX:+ThraceClassLoading 参数运行这段代码,查看日志,即可以看到Child类确实被加载了,只是初始化没有进行
例3 :引用final常量
public class FinalFieldClass{
public static final String constString = "CONST";
static{
System.out.println("FinalFieldClass init");
}
}
public class UseFinalField{
public static void main(String[] args){
System.out.println(FinalFieldClass.constString);
}
}
运行代码:CONST
FinalFieldClass类没有由于其常量字段constString被引用而初始化,这是由于在Class文件生成时,final常量因为其不变性,作了适当的优化。
分析UseFinalField类生成的Class文件,能够看到main函数的字节码为:
在字节码偏移3的位置,经过Idc将常量池第22项入栈,在此Class文件中常量池第22项为:
#22 = String #23 //CONST
#23 = UTF8 CONST
由此能够看出,编译后的UseFinalField.class中,并无引用FinalFieldClass类,而是将其final常量直接存放在常量池中,所以,FinalFiledClass类天然不会被加载。(javac在编译时,将常量直接植入目标类,再也不使用被引用类)经过捕获类加载日志(部分日志)能够看出:
注意:并非在代码中出现的类,就必定会被加载或者初始化,若是不符合主动使用的条件,类就不会被初始化。
1.2 类装载的整个过程
1)加载类
加载类处于类装载的第一个阶段。
加载类时,JVM必须完成:
2)链接
1 验证类:
当类被加载到系统后,就开始链接操做,验证是链接的第一步。
主要目的是保证加载的字节码是符合规范的。验证的步骤如图:
2 准备
当一个类验证经过后,虚拟机就会进入准备阶段,在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。
java虚拟机为各类类型变量默认的初始值如表:
类型 | 默认初始值 |
int | 0 |
long | 0L |
short | (short)0 |
char | \u0000 |
boolean | false |
reference | null |
float | 0f |
double | 0f |
注意:java并不支持boolean类型,对于boolean类型,内部实现是Int,因为int的默认值是0,故对应的,boolean的默认值是false
若是类属于常量字段,那么常量字段也会在准备阶段被附上正确的值,这个赋值属于java虚拟机的行为,属于变量的初始化。事实上,在准备阶段,不会有任何java代码被执行。
3 解析类
在准备阶段完成后,就进入了解析阶段。
解析阶段的任务就是将类、接口、字段和方法的符号引用转为直接引用。
符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关。比较容易理解的就是在Class类文件中,经过常量池进行大量的符号引用。
具体能够使用JclassLib软件查看Class文件的结构:::
3)初始化
初始化时类装载的最后一个阶段。若是前面的步骤没有出现问题,那么表示类能够顺利装载到系统中。此时,类才会开始执行java字节码。
初始化阶段的重要工做是执行类的初始化方法<clinit>。方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。
例如:
public class SimpleStatic{
public static int id = 1;
public static int number;
static{
number = 4;
}
}
java编译器为这段代码生成以下的<clinit>:
0 iconst_1
1 putstatic #2 <Demo.id>
4 iconst_4
5 putstatic #3 <Demo.number>
8 return
能够看出,生成的<clinit>函数中,整合了SimpleStatic类中的static赋值语句以及static语句块,前后对id和number两个成员变量进行赋值
因为在加载一个类以前,虚拟机老是会试图加载该类的父类,所以父类的<clinit>老是在子类<clinit>以前被调用。也就是说,子类的static块优先级高于父类。
public class ChildStatic extends Demo{
static{
number = 2;
}
public static void main(String[] args){
System.out.println(number);
}
}
运行可知:
2
说明父类的<clinit>老是在子类<clinit>以前被调用。
注意:java编译器并非为全部的类都产生<clinit>初始化函数,若是一个类既没有赋值语句,也没有static语句块,那么生成的<clinit>函数就应该为空,所以,编译器就不会为该类插入<clinit>函数
例如:
public class StaticFinalClass{
public static final int i=1;
public static final int j=2;
}
因为StaticFinalClass只有final常量,而final常量在准备阶段初始化,而不在初始化阶段处理,所以对于StaticFinalClass类来讲,<clinit>就无事可作,所以,在产生的class文件中没有该函数存在。