1. 什么是类加载
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
2. 类加载生命周期
3. 类加载过程
区分"加载" 和"类加载","加载"是类加载的一部分。
加载阶段虚拟机需要做的事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类加载器
第一个阶段就是读取class文件,class文件可已从多种途径来,如zip,jar,jsp等。虚拟机设计团队将第一阶段java虚拟机外部实现,以便让程序自行决定如何获取需要的类。实现这个动作的代码模块称为"类加载器"。
类加载器种类
- 启动类加载器(BootStrap ClassLoader):在hotspot虚拟机中,使用c++语言编写,是虚拟机的一部分。作用是将
/lib或者被-Xbootclasspath参数指定的,并且是虚拟机识别的(文件名判断,如rt.jar)。启动类加载器由java null标识。 - 扩展类加载器(Extension ClassLoader):加载
/lib/ext下的类,或者是被系统变量java.ext.dirs指定的类库,是sun.misc.Launcher$ExtClassLoader的实例,可以用户直接使用。 - 应用程序类加载器(Application ClassLoader):又称为系统类加载器,用于加载classpath上的类库。是sun.misc.Launcher$AppClassLoader的实例。默认的应用程序类加载器的父类加载器是扩展类加载器。如果程序中没有自定义类加载器,这就是程序的默认类加载器。
可以看到启动类加载器由null表示,扩展类加载器和系统类加载器是不同的。
双亲委派模型
ClassLoader中loadClass(String name,boolean resolve)方法是ClassLoader里用来载入类的方法,可以在实现自定义的classloader的时候由用户重写。当jvm请求一个java类的时候,他向ClassLoader中的loadClass函数中传入类的全限定名。java里就是靠着java.lang.ClassLoader中这部分代码实现双亲委托模型的。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//尝试从已加载类中找到目标类
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//调用上层类加载器来加载类
c = parent.loadClass(name, false);
} else {
//使用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//调用实现类重写的findClass方法
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
loadClass函数可以由用户重写来破坏双亲委派模型,findClass函数可以由用户重写来指定获取类文件的方式。sun.misc.Launcher$AppClassLoader继承了java.net.URLClassLoader。具体的findClass方法就是在这里实现的
protected Class> findClass(final String name)
throws ClassNotFoundException
{
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Class run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
throw new ClassNotFoundException(name);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
}
这里出现了父加载器,是因为启动类加载器,扩展类加载器和应用程序类加载器是有父子关系的。
双亲委派模型要求除了顶层启动类加载器,其余类加载器都应当有自己的父类加载器。双亲委派模型不是强制性约束。
工作流程:
如果一个类加载器,那么这个类加载器首先会将请求委派给父类加载器,因此所有的类加载请求都会传送到顶层的启动类加载器中,只有当父类加载器无法完成类加载时,子加载器才会尝试自己加载。
优点:
java类随着它的类加载器有了带有优先级的层级关系。例如java.lang.Object无论哪个类加载器要加载这个类,最终都委派给启动类加载器加载,这就保证了系统只有一个java.lang.Object类。
类和类加载器确立的唯一性
对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立这个类在虚拟机中的唯一性,每一个类加载器都拥有独立的类名称空间。
如果类加载器不同,那么代表类的Class对象的如下方法返回结果也不相同
//等于instanceof 运算符
public native boolean isInstance(Object obj);
//确定调用这个方法的class或者interface是不是参数里cls的父类或者父接口
public native boolean isAssignableFrom(Class> cls);
//表明其他类是否等于obj
public boolean equals(Object obj) {
return (this == obj);
}
破坏双亲委派模型
1.线程上下文类加载器
2.OSGI热插拔,模块热部署
数组类的加载
数组类并不通过类加载器创建,它由java虚拟机直接创建。但是数组类的元素类型(Element Type指数组去掉所有维度的类型)的字段最终需要靠类加载器创建,数组类C的创建过程遵循如下规则:
- 如果数组的组件类型(Component Type,指数组去掉一个‘[’的类型)是引用类型,就递归调用类加载过程,数组类C将在加载该组件类型的类加载器的类名称空间上被标识
- 如果数组的组件类型不是引用类型(如int),Java虚拟机将会把数组C标记为与引导类加载器相关联
- 数组类的可见性与它的组件类型可见性一致,如果组件类型不是引用类型,拿数组类的可见性将默认为public。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。然后在内存中实例化一个java.lang.Class对象,不同虚拟机对这个区域的规定不同,hotspot虚拟机Class队形存放在方法区中,这个对象将作为程序访问方法区中这些类型数据的外部接口。
连接
验证、准备、解析3个部分统称为连接,加载阶段的内容与连接阶段的内容是交替进行的。加载过程没有完成,连接阶段可能已经开始,如验证阶段。
验证
1.文件格式验证
验证字节流是否符合class文件格式的规范,并且能被当前版本的java虚拟机处理。
2.元数据验证
对字节码描述的信息进行语义分析,保证其描述的信息符合java语言规范的要求。例如是否继承了被final修饰的类。
3.字节码验证
对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。
通过字节码验证的类方法不一定是安全的,但没通过验证的一定不安全。
例如保证类型转换。
4.符号引用验证
发生在虚拟机将符号引用转化为直接引用的时候(解析阶段)。符号引用验证是对类自身以外常量池中各种符号引用的信息进行匹配校验。目的是确保解析动作能够被正确执行。
准备
准备阶段是正式类变量分配内存并设置初始值的时候。这里的类变量是static变量,初值的意思是数据类型的零值。但如果类变量被final修饰了,那么编译时javac将为该变量生成ConstantValue属性,在准备阶段该变量就会被附上指定的值。引用类型的零值是null。
解析
解析阶段是虚拟机将常量池内的符号引用转化为直接引用的过程。
- 符号引用:类和接口的全限定名、字段的名称和描述符(类型标识,包括基本类型和对象)。
- 直接引用:直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。如果有了直接引用,那引用目标必定已经在内存中存在。
虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将被使用前才去解析它。可以对同一个符号引用进行多次解析请求,虚拟机可以实现对第一次解析结果进行缓存。
- 类或接口的解析
在类D中,要将一个从未解析过的符号引用N解析成一个类或接口C的直接引用,需要经过如下步骤:
- 如果C不是一个数组类型,那虚拟机会将N的全限定名传递给D的类加载器去加载类C。在这个过程中,由于元数据验证、字节码验证的需要,可能触发其他相关类的加载动作。
- 如果C是一个数组类型,并且数组的元素类型为对象,那么就会按第1点规则去夹杂数组元素类型。然后由虚拟机生成一个代表此数组维度和元素的数组对象。
- 解析完成之前进行符号引用验证,确认D是否具有对C的访问权限,如果没有则抛出异常。
- 字段解析
首先对字段所属的类C进行解析,如果这个字段是在C中定义的就返回这个字段的直接引用,否则去查找其父类或者接口。
public class TestClass {
public Integer firstArg = 1;
}
使用javap进行反编译后
Classfile /C:/Users/whisper/eclipse-workspace/javabase/bin/TestClass.class
Last modified Feb 17, 2018; size 387 bytes
MD5 checksum 6fca7d4356a7b7110b5193b55b284801
Compiled from "TestClass.java"
public class TestClass
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // TestClass
#2 = Utf8 TestClass
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 firstArg
#6 = Utf8 Ljava/lang/Integer;
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."":()V
#11 = NameAndType #7:#8 // "":()V
#12 = Methodref #13.#15 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#13 = Class #14 // java/lang/Integer
#14 = Utf8 java/lang/Integer
#15 = NameAndType #16:#17 // valueOf:(I)Ljava/lang/Integer;
#16 = Utf8 valueOf
#17 = Utf8 (I)Ljava/lang/Integer;
#18 = Fieldref #1.#19 // TestClass.firstArg:Ljava/lang/Integer;
#19 = NameAndType #5:#6 // firstArg:Ljava/lang/Integer;
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 LTestClass;
#24 = Utf8 SourceFile
#25 = Utf8 TestClass.java
{
public java.lang.Integer firstArg;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC
public TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object."":()V
4: aload_0
5: iconst_1
6: invokestatic #12 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: putfield #18 // Field firstArg:Ljava/lang/Integer;
12: return
LineNumberTable:
line 2: 0
line 3: 4
line 2: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this LTestClass;
}
SourceFile: "TestClass.java"
如#18 = Fieldref #1.#19 // TestClass.firstArg:Ljava/lang/Integer;
所示,TestClass是所属类,firstArg是简单名称,Ljava/lang/Integer是描述符,L代表Object对象。
- 类方法解析
和字段解析一样,首先对方法所属类C进行解析,然后
- 如果C是接口,抛出java.lang.IncompatibleClassChangeError异常。
- 如果C中有简单名称和描述符都与目标相匹配的方法,如果有返回这个直接引用。
- 在C的父类中递归查找简单名称和描述符都与目标相匹配的方法,返回这个方法的直接引用。
- 在C的实现的接口中递归查找,如果找到说明该方法是抽象的,所以C是个抽象类,抛出java.lang.AbstractMethodError异常。
- 否则宣告查找失败,抛出java.lang.IlllegalAccessError异常。
- 接口方法解析
首先对方法所属的接口C,如果C中有简单名称和描述符都和目标相匹配的方法,如果有返回这个方法的直接引用,否则在父接口中递归查找。
初始化
初始化阶段第一次真正执行程序中的代码。在准备阶段有已经为类变量赋过初值,初始化阶段根据程序员通过程序制定的主管计划去初始化类变量和其他资源,或者可以从另一个角度表达:初始化阶段是执行类构造器
- 类构造器
()
()是由编译器自动收集类中所有类变量的赋值操作和静态语句块中语句合并产生,收集顺序由代码顺序决定。静态语句块中只能访问到定义在静态语句块之前的变量,之后的变量只能赋值但不能访问。执行的顺序是代码顺序。
如下赋值操作是被允许的,但用sysout会出错
如下会先执行firstArg=100,在执行firstArg=1
()方法与类的构造函数(与实例构造器 ())不同,它不需要显示地调用父类构造器,虚拟机会保证在子类 ()方法执行之前,父类的 ()方法已经构造完毕。因此在虚拟机中第一个被执行的 ()方法的类肯定是java.lang.Object。 - 由于父类的
()方法先执行,意味着父类中定义的静态语句块会优先子类执行。 方法对于类或者接口来说不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 ()方法。 - 接口中没有静态语句赋值块,但仍然有变量初始化的赋值操作(接口中的成员变量都是static final并且需要显式初始化)。与类不同,执行接口的
方法不需要先执行父接口的 ()方法,只有当父接口中的定义的变量要使用时,父类才会初始化。另外,接口的实现类在初始化的时候也不会执行接口的 ()方法。 - 虚拟机会保证一个类的
()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待。
public class TestDLC {
static class DeadLoopClass{
static {
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass...");
while(true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + "run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
执行结果
静态代码块的执行时机
静态代码块形如,需要加static关键字
public class TestClass{
static{
...
}
}
静态代码块的执行就是在类加载阶段中的初始化环节进行,虚拟机规范严格规定了有且只有5中情况必须立即对类进行初始化:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,常见的场景:使用new关键字实例化对象,读取或者设置一个类的静态字段(被final修饰的静态字段除外),调用一个类的静态方法的时候。
public class TestStaticInitializaField {
public static class A{
public static int a = 100;
static{
System.out.println("i am A..");
}
}
public static class B{
public static void say(){
System.out.println("B.say...");
}
static {
System.out.println("i am B..");
}
}
public static class C{
static {
System.out.println("i am C..");
}
}
public static void main(String[] args) {
int b = A.a;
B.say();
C c = new C();
}
}
结果:
- 使用java.lang.reflect的时候,如果类没有执行过初始化需要先触发其初始化。
package daiwei.testjvm;
public class TestStaticInitializaField {
public static class C{
static {
System.out.println("i am C..");
}
}
public static void main(String[] args) {
//这样是不会初始化的
System.out.println(C.class+ "will not initialize");
try {
Class clazz = Class.forName("daiwei.testjvm.TestStaticInitializaField$C");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
结果
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
package daiwei.testjvm;
public class TestStaticInitializaField {
public static class Super{
static {
System.out.println("i am super ..");
}
}
public static class Child extends Super{
static {
System.out.println("i am child");
}
}
public static void main(String[] args) {
Child child = new Child();
}
}
结果:
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化主类。
package daiwei.testjvm;
public class TestStaticInitializaField {
static{
System.out.println("i am TestStaticInitializaField");
}
public static void main(String[] args) {
}
}
结果:
5.当使用JDK1.7的动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putstatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5中会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语,"有且仅有",这5中场景中的行为被称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
以下是被动引用的例子:
package daiwei.testjvm;
public class TestStaticInitializaField {
/*
* 通过子类引用父类的静态字段,不会导致子类的初始化
* 只有直接定义这个字段的类才会被初始化
*/
public static class Super{
public static int a = 0;
static {
System.out.println("i am super ..");
}
}
public static class Child extends Super{
static {
System.out.println("i am child");
}
}
public static void main(String[] args) {
System.out.println(Child.a);
}
}
通过jvm参数-XX:+TraceClassLoading参数观察到Super和Child类都被加载了,但只有父类的初始化被执行了。
package daiwei.testjvm;
public class TestStaticInitializaField {
/*
* 通过数组定义来引用类不会触发此类的初始化
*/
public static class C{
static {
System.out.println("i am C..");
}
}
public static void main(String[] args) {
C[] classes = new C[10];
}
}
这段代码不会触发C的初始化,但它触发了数组类的初始化,数组类由虚拟机自动生成。
package daiwei.testjvm;
public class TestStaticInitializaField {
/*
* 常量在编译阶段就已经加入到类的常量池中,本质上并没有直接引用到定义常量的类
* 因此不会触发类的初始化。
*/
public static class B{
public static final String a = "hello...";
static {
System.out.println("i am B..");
}
}
public static void main(String[] args) {
System.out.println(B.a);
}
}
结果:
可以看到B的加载都没有触发。
关于sun包源码无法调试
sun包源码地址
非静态内部类无法定义静态代码块