类加载阶段
有三个阶段,分别是加载
、链接
、初始化
。
加载
Java类编译成字节码以后,运行呢需要通过类加载器把这个类的字节码要加载到方法区中。加载到方法区后底层是用c++的数据结构来描述Java的class类。这个数据结构的名称叫instanceKlass
,Java是不能直接访问instanceKlass
,他中间呢需要进行一个转换,即有一个转换的过程。
instanceKlass的重要域(field)有:
- _java_mirror Java类的镜像,这个镜像起到桥梁的作用,咱们的Java对象要想访问这个instanceKlass的信息,他得通过镜像来访问Klass。
相当于这个镜像是c++的数据结构跟Java对象之间的一个桥梁。 - _super 即表示instanceKlass的父类
- _fields 代表这个instanceKlass都有哪些成员变量的定义
- _methods 表示他里面有哪些方法定义
- _constants 表示常量池
- _class_loader 表示哪个类加载器加载了他
- _vtable 虚方发表,各个方法的入口地址
- _itable 接口的方发表
那我们以前所理解的XX.class,他并不是Klass,而是指的是mirror镜像,比如对String来说,String在我们的方法区有一个instanceKlass这样一个c++的数据结构来描述,但你平时用Java代码去访问的时候,他不能直接访问instanceKlass,她只能是先找到String.class,String.class实际上就是instanceKlass的镜像,他两之间互相持有对方的指针。那如果我们通过一个Java对象想去访问String.class,你得先访问这个镜像对象(String.class),镜像才能进一步去访问到instanceKlass,然后间接的知道他内部有哪些成员变量、方法、虚方法表、接口方法表他的入口地址都是什么等等。所以相当于镜像(String.class)起到了一个桥梁的作用。
需要注意的是如果这个类还有父类没有加载,那他会触发父类的加载,就是说他必须先保证他的父类以及他实现的一些接口先加载,然后我本身这个类才会加载。另外要注意就是我们的‘加载’和‘链接’并不是说一个先完成才能进行下一个,而加载和链接这两个步骤可能是需要交替运行的。
【图片转自黑马】
这张图是用Person类(绿色部分)和Person对象(右边两个黄色)来表示他们之间(Person类、Person对象、instanceKlass)的关系。之前介绍过,类加载到方法区,当然我们现在用的是jdk1.8,方法区的实现就是一个叫Metaspace(元空间)的实现,所以我们的类的那些字节码都会被加载到元空间中,就是构成了刚才提到的instanceKlass这样一个数据结构。加载的同时,他就会在Java堆内存中,生成一个_java_mirror镜像,就是会产生一个Person.class,就是俗称的类对象,这个类对象是在堆里面存储的,但是他持有了刚才instanceKlass的内存的指针地址,反过来,instanceKlass里面的_java_mirror也持有Person.class的内存地址,这是class类对象跟instanceKlass之间的关系。
类:通过代码描述类的属性、方法等内容,实现类的定义;
类对象:类定义后即创建类对象,包括定义的类的各种属性和方法;
实例对象:基于类创建各个实例对象,实例对象继承类的所有属性和方法。
那我们如果以后用new关键字创建了一系列的Person的实例对象,那么他是怎么联系起来的呢,比如说我有两个Person实例对象(右边两个上下黄色),每个实例对象他都有自己的对象头(16个字节),其中8个字节对应着他这个对象的class地址,如果你想通过这个对象获取他的class信息,那就会去访问这个对象的对象头,然后去通过这个地址先找到_java_mirror(即Person.class),再通过类对象再间接的去元空间找到instanceKlass,这样的话当我们去调用类对象的getXX方法时,他实际上都是到元空间里面获得这些_fields、_methods等等这种具体的信息。这是实例对象和类对象及跟我们的instanceKlass之间的关系。
instanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中。
链接
连接阶段分三个小步骤:验证
、准备
、解析
。
(1)验证
验证类(字节码)是否符合JVM规范,比如加载进来的是格式不正确的字节码是不行的,当然还会进行安全性检查。(比如用二进制编辑器[UltraEdit等支持二进制编辑的编辑器]去修改HelloWorld.class(helloworld字节码)的魔术,在控制台cmd运行该class文件后,就会报错[java.lang.ClassFormatError异常]。
)这样就能阻止不合法的类运行。
(2)准备
为static变量分配空间,设置默认值。(比如我们有一个整形的静态变量,就会给他分配四个字节,并且把他默认值都填充为0。
)
- static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾。从上面的图中(jdk1.8的内存结构部分)可以看出,静态变量是存储在哪呢,他是跟在类对象(绿色)的后面(浅蓝色),跟类对象存储在一起,都存储在堆中(1.7也是)。而在早期的jvm里(jdk6以前),静态变量是跟在instanceKlass后面,一起存储在方法区中。这是关于他的存储位置。
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。赋值的动作发生在类的构造方法中,类的构造方法在初始化阶段被调用。所以分配空间和赋值是两个不同的操作。比如编译 static int a;的话,字节码中只看到声明了a,没有赋值。再比如static int b = 10;的话,编译后字节码中有声明也有在构造方法中赋值。
- 如果static变量是final的基本类型(以及字符串常量),那么编译阶段值就确定了,赋值在准备阶段完成。比如是static final int c = 10;的话由于是final修饰的,反编译后字节码中可以看得出在c的声明部分中就有ConstantValue:int 10,但在类初始化方法中是没有给c赋值的语句,说明final的赋值动作并不是发生在后续初始化阶段,而是在准备阶段就完成赋值了。相当于编译期间就知道它的值,而且它的值将来不会变了。如果是static final String d = “aasd”;的话,现在这个d的赋值也不是初始化时完成,他和上面的static final int c = 10一样在准备阶段就已经知道了并确定了值为“asd”,所以不必等到初始化时再完成赋值了。
- 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成。但是 static final Object e = new Object()的话,由于是要创建新的对象,所以必须等到类初始化好了才能执行,因此赋值操作出现在类构造方法中。
(3)解析
将常量池中的符号引用解析为直接引用。
通过下面例子去理解什么是解析:
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("com.cnm.C");
// 第二种情况
// C c = new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
这里有类C和D,默认情况下,类的加载都是一个懒惰式的,即你用到了类C,没有用到类D的话,那么类D是不会主动加载的(D d = new D()
)。在main方法中准备了类加载器(ClassLoader
),调用了他的loadClass方法去加载类C,这里需要注意的是,调用loadClass记载类C的时候,只会进行类C的加载,并不会导致类C的解析以及初始化,从而类D也不会被加载、解析、初始化,这是一种情况(简单的话C被加载到虚拟机,但D没被加载到虚拟机
)。另外一种情况是,假如在main方法里直接new C()的话,这时候因为new会触发类的包括加载、解析、初始化,所以他也会间接的会让C中的D属性加载解析初始化(这时候C和D都被虚拟机加载进来,也就是两个都被解析了
)。用工具查看时,如果是第一种情况,C的常量池中的D是符号引用(还没被解析,此时虚拟机并不知道这个符号到底在内存的哪个位置
),而第二种情况下,D是直接引用(此时能够确切的之后这个类或方法或属性在内存中的位置了
)。
初始化
实际上就是去执行类的构造方法,即< cinit >()V
,虚拟机会保证这个类的构造方法的线程安全。
初始化发生的时机
概括的说,类的初始化是懒惰的。
- main方法所在的类,总会被首先初始化。(
因为他是整个程序的入口
) - 首次访问这个类的静态变量或静态方法时也会导致这个类的初始化。
- 子类初始化,如果父类还没初始化,会引发父类的初始化。(
联动父类的初始化
) - 子类访问父类的静态变量,只会触发父类的初始化。
- 执行 Class.forName时,默认情况下是会导致类的初始化。
- new一个对象时会导致这个对象的类进行初始化。
不会导致初始化的情况
- 当你访问类的 static final类型的静态常量(
基本类型和字符串
),不会触发类的初始化。(因为基本类型和字符串的赋值在类的连接的准备阶段就完成了,而不是触发阶段才完成的。
) - 访问“类对象.class”不会触发初始化。(
“类对象.class”是在类加载时会生成mirror对象,并不是在初始化阶段完成的,所以你访问“类对象.class”不会触发初始化操作
) - 创建该类的数组时不会触发初始化。
- 调用类加载器的loadClass方法时不会导致类的初始化。
- Class.forName的参数2为false时不会导致类的初始化。
public class Demo {
static {
System.out.println("main初始化");
}
public static void main(String[] args) throws ClassNotFoundException {
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
如果运行该类的话,由于main方法所载的类会先被初始化,所以就会输出"main初始化"。
下面的是main方法里面的代码例子(每段代码执行时最好把其他段代码都注释
):
public static void main(String[] args) throws ClassNotFoundException {
/*
不导致初始化的情况
*/
// 1,静态常量不会触发初始化
// b是final static,所以调用他不会导致B的初始化。比如运行后,虽然会打印b,但B里面的static代码块并没执行。说明B没被初始化。(其实就看静态代码块儿就知道了)
System.out.println(B.b);
// 2,调用“类对象.class” 不会触发B的初始化
System.out.println(B.class);
// 3,创建该类的数组不会导致B的初始化
System.out.println(new B[0]);
// 4,不会初始化类B,但会加载B、A
// 之前说过,ClassLoader只会导致类的记载,但解析、初始化都不会执行。所以B的静态代码块儿仍然没执行。
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
c1.loadClass("com.cnm.B");
// 5,不会初始化类B,但会加载B、A(B的父类)
// 虽然还是ClassLoader,但这回调用了Class.forName,参数2是false,即不初始化。结果同上。
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("com.cnm.B",false,c2);
/*
能初始化的情况
*/
// 1,首次访问这个类的静态变量或静态方法时(触发了A的初始化,打印了"a init")
System.out.println(A.a);
// 2,子类初始化,如果父类还没初始化,会引发。
// 调用B的static boolean c = false,由于c是静态变量,但他不是final,所以类B会被初始化,但B又继承了A,虽然此时类A还没
// 被初始化,但类B的初始化会间接的让类A也初始化,而且父类初始化在子类之前。所以输出了a init和b init。
System.out.println(B.c);
// 3,子类访问父类的静态变量,只触发父类初始化,所以只会输出 a init
System.out.println(B.a);
// 4,会初始化类B,并先初始化类A。默认的参数2是true。输出结果和上面的“2”一样。
Class.forName("com.cnm.B");
}
小例子
从字节码的角度去分析,看看使用a b c这三个常量是否会导致类E初始化:
public class Demo {
public static void main(Striing[] args) {
System.out.println(E.a);// E的静态代码块儿没被执行
System.out.println(E.b);// 同上
System.out.println(E.c);// E的静态代码块儿被执行
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
static {
System.out.println("e init");
}
}
从结论上来说,使用a b是不会导致E初始化,因为它两都是静态常量,而且是基本类型和字符串常量,他们都是在类连接的准备阶段去进行赋值的。而c不一样,因为c不是基本类型,而是包装类型,他底层会自动的做装箱操作,会调用Integer.valueOf(20),把基本类型20转换成包装类型,所以这个只能推迟到初始化阶段完成。
字节码中的一部分如下:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2,locals=0,args_size=0
0: bipush 20
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #3 // Field c:Ljava/lang/Integer;
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String init E
13: invokevirtual #6
16: return
...
public static final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
public static final java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String hello
public static final java.lang.Integer c;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
...
可以看得出,字节码的静态代码块儿中,也就是类的构造方法里,可以看到有一个Integer的调用了valueOf方法把20进行装箱,变成integer对象,putstatic是给静态常量c赋值,下面的两行是e init这个打印语句。所以可以看到c确实是在初始化阶段执行的,但是a和b在链接的阶段被固定死了,而c是没办法确定他具体的值(没有ConstantValue
)。
接下来是典型应用 - 完成懒惰初始化式的单例模式:
public class Demo {
public static void main(String[] args) {
Singleton.getInstance();// 只有第一次调用下面的getInstance方法时,才会触发LazyHolder的加载、连接、初始化。
}
}
class Singleton {
// 在外部访问该静态方法,并不会触发LazyHolder的初始化,所以不会创建Singleton对象,只有初始化LazyHolder时才创建。
public static void test() {
println("test");
}
private Singleton(){};// 第一步让他作成单例,不让别人实例化他。
// 这里我们利用类加载的一个特性,即第一次用这个类时才会触发这个类的加载、连接、初始化。所以这里创建一个静态的内部类。
// 静态内部类的好处是他可以访问外部类(Singleton)的资源,比如构造方法或方法之类的。
private static class LazyHolder {
private static final Singleton SINGLETON = new Singleton();
static {
System.out.println("lazy holder init");
}
}
/*
为什么这样实现懒惰效果呢?比如,如果你没有去调用getInstance(),相当于你根本就不会用到这个LazyHolder,既然你不会用到LazyHolder,
那对于LazyHolder来讲,并不会触发他的加载、连接以及初始化。(所以调用getInstance时才会初始化LazyHolder)
第二步让他变成懒惰的,第一次用到时再创建对象。这样可以节省一些内存的开销。
*/
public static Signleton getInstance() {
return LazyHolder.SINGLETON;
}
}
这个线程安全问题是可以保障的,因为静态内部类他在初始化的过程中他的给静态变量赋值或他的静态代码块儿的操作是有类加载器能够保证他的线程安全性的,所以肯定是线程安全的。