深入理解JVM(一)类加载器部分:初始化规则、单例对象分析
单例对象
eg1:
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton=Singleton.getInstance();
System.out.println("Singleton3 counter1="+Singleton.counter1);
System.out.println("Singleton3 counter2="+Singleton.counter2);
}
}
class Singleton{
public static int counter1=1;
private static Singleton singleton = new Singleton();
private Singleton(){
System.out.println("Singleton1 counter1="+counter1);
System.out.println("Singleton1 counter2="+counter2);
counter1++;
counter2++;
System.out.println("Singleton2 counter1="+counter1);
System.out.println("Singleton2 counter2="+counter2);
}
public static int counter2=2;
public static Singleton getInstance(){
return singleton;
}
}
//输出:
Singleton1 counter1=1
Singleton1 counter2=0
Singleton2 counter1=2
Singleton2 counter2=1
Singleton3 counter1=2
Singleton3 counter2=2
过程分析:首先 Singleton singleton=Singleton.getInstance(); 调用了静态方法,是对Singleton的主动使用,会导致Singleton的初始化
在类的加载、连接、初始化中,在连接阶段有一个准备阶段,在此阶段会为静态变量赋一个默认值(并非指定的值),所以在连接阶段上述的静态变量的默认值如下
连接阶段的默认值:
public static int counter1=0;
private static Singleton singleton = null;
public static int counter2=0;
在初始化步骤,JVM会为静态变量赋指定的值,此时静态变量赋值顺序与定义的顺序一致,即赋值情况如下:
public static int counter1=1;
private static Singleton singleton = new Singleton();
//此时会调用Singleton的私有构造方法 private Singleton()
//在私有构造方法中,在执行++之前
//counter1 已经在前面被赋值为1(因为counter1 定义在singleton之前)
//此时counter2仍然是0(因为counter2 定义在singleton之后前,并未赋指定值)
//所以第一组输出为:
> Singleton1 counter1=1
> Singleton1 counter2=0
//在执行++后,
counter1=2
counter2=1
//所以第二组输出为:
> Singleton2 counter1=2
> Singleton2 counter2=1
//经过上述步骤singleton赋值完毕,接下来对counter2赋值
//赋值后
public static int counter2=2;
//经过上述步骤,初始化步骤完成,初始化后各个静态变量的值为
counter1=2;
singleton = Singleton的实例对象;
counter2=2;
//所以第三组输出为:
> Singleton3 counter1=2
> Singleton3 counter2=2
类加载到实例化对象过程
加载
- 加载:就是把二进制的java类型读入java虚拟机中(.class)
类加载的最终产品是位于内存中的Class对象
Class对象封装了类在方法去内的数据结构,并向Java程序员提供了访问方法区内的数据结构借口
- 有两种类加载器
- Java虚拟机自带的类加载器
根类加载器(Bootstrap)
扩展类加载器(Extension)
系统(应用)类加载器(Sysytem)
- 用户自定义的类加载器
java.lang.ClassLoader的子类
用户可以定制类的加载方式
- 类加载器并不需要等到某个类被”首次主动使用“是再加载它
JVM规范允许类加载器在预料到某个类将要被使用是预先加载它,如果在预先加载过程中遇到.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报错(LinkageError错误)
如果这个类一直没有被主动使用,那么类加载器就不会报告错误。
连接
连接:将已经读入内存的类的二进制数据合并到虚拟机的运行时环境中去,主要包含三个步骤:
- 验证: 验证.class文件的正确性
验证内容:
类文件的结构检查
语义检查
字节码验证
二进制兼容性的验证
- 准备: 为类变量(静态变量)分配内存,设置默认值,直到初始化之前,类变量都没有被赋值成真正的初始值(我们指定的初始值,参考单例模式部分案例)
- 解析:在类型的常量池中寻找类、接口、字段和方法的符号引用,并将这些符号引用替换成直接引用的过程
初始化
- 初始化:为类变量赋值为真正的初始值(我们指定的,如果没有指定则使用默认的初始值)
静态变量的声明语句、静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来执行它们。
- 类的初始化步骤
如果这个类还没有被加载和连接,那么就先进行加载和连接;
假如这个类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类;
加入类中存在初始化语句,那就依次执行这些初始化语句
类初始化时机
当Java虚拟机初始化一个类时,要求它所有的父类都已经被初始化,但是这条规则不适用于接口
一个父接口不会因为它的子接口或者它的实现类的初始化而初始化,只有当程序首次使用定接口特的静态变量时,才会导致该接口的初始化。(特定的变量指的是的变异期间不确定,运行期间才确定的变量)
eg1:
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
}
interface MyParent5 {
public static Thread thread = new Thread() {//匿名内部类
{
System.out.println("MyParent5 block");
}
};
}
class MyChild5 implements MyParent5 {
public static int b = 6;
{
System.out.println("MyChild5 static block");
}
}
//输出结果:
[Loaded com.aaa.test.MyTest5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
MyChild5 static block
6
//注:MyParent5$1 为MyParent5中的匿名内部类
从上面的结果可以看出,当实现类初始化的时候,虽然父接口被加载,但是却没有初始化
eg2:
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.c);
}
}
interface MyParent5 {
public static Thread thread = new Thread() {//匿名内部类
{
System.out.println("MyParent5 block");
}
};
}
interface MyChild5 extends MyParent5 {
public static int b = 6;
public static String c = UUID.randomUUID().toString();
public static Thread thread = new Thread() {//匿名内部类
{
System.out.println("MyChild5 block");
}
};
}
//输出结果:
[Loaded com.aaa.test.MyTest5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
MyChild5 block
4ebcde87-e2cc-4e47-81fa-eb1c557d5ca4
当子接口初始化的时候,如果没有调用父接口的非编译期间指定变量,父接口不会初始化
eg3:
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.a);
}
}
interface MyParent5 {
public static String a = UUID.randomUUID().toString();
public static Thread thread = new Thread() {//匿名内部类
{
System.out.println("MyParent5 block");
}
};
}
interface MyChild5 extends MyParent5 {
public static int b = 6;
public static String c = UUID.randomUUID().toString();
public static Thread thread = new Thread() {//匿名内部类
{
System.out.println("MyChild5 block");
}
};
}
//输出结果:
[Loaded com.aaa.test.MyTest5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
MyParent5 block
20e41ba5-939a-485f-a45c-0682963c88f1
通过子接口调用父接口的非编译期间可指定变量时,父接口,子接口都会被加载,但是只有父接口会被初始化
如果调用的是接口编译期间可指定的变量,此时该变量存在调用类的常量池中。父接口,实现类都不会加载与初始化。
类实例化
- 为新的对象分配内存(堆)
- 为实例变量赋默认值(类似类变量先赋默认值,非指定)
- 为实例变量赋正确的初始值(类似类变量先赋指定值定)
- java编译器会为它编译的每一个类都至少生成一个实例初始化方法(即构造函数),在java的.class文件中,这个实例化初始化方法被称为<init>。针对源代码中每一个类的构造方法,编译器都产生一个<init>方法
本文为学习张龙老师深入理解JVM的笔记与心得,转载请注明出处!!!