先看一个示例:
package com.lory.jvm;
class Singleton {
private static Singleton singleton = new Singleton();
public static int count1;
public static int count2 = 0;
private Singleton(){
count1++;
count2++;
}
public static Singleton getInstance(){
return singleton;
}
}
public class MyTest{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("count1= "+singleton.count1);
System.out.println("count2= "+singleton.count2);
}
}
答案:
count1= 1
count2= 0
Java虚拟机与类的生命周期:
在如下情况,虚拟机会结束其生命周期:
- 执行了System.exit()方法。
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误程序终止
- 由于操作系统错误导致虚拟机结束
类的加载,连接与初始化:
加载:查找并加载类的二进制文件。
连接:
1.验证:确保被加载类的正确性。
2.准备:为类的静态变量分配内存,并将其初始化为默认值。
3.解析:把类的符号引用转换为直接引用。
初始化:为类的静态变量赋予正确的初始值。
Java程序对类的使用方式可以分为两种:
1.主动使用
2.被动使用
所有的Java虚拟机实现必须在每个类或者接口被Java程序首次主动使用时才初始化他们。
Java程序主动使用(类的初始化时机)可以分为六种:
- 创建类的实例。
- 访问或使用(赋值)类或接口的静态变量。
- 调用类的静态方法。
- 反射(如:Class.forName("Test"))。
- 初始化一个类的子类。
- Java虚拟机启动时被标明为启动类的类。
除了以上六种情况,其它使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化。
类的加载是指将类的.class文件中的二进制数据读入内存当中,将其放在运行时数据区的方法区内,然后在堆区当中创建一个java.lang.Class的对象,用来封装类在方法区当中的数据结构。
加载.class文件的方式:
- 从本地系统当中直接加载。
- 通过网络下载.class文件。
- 从zip,jar等归档文件当中加载。
- 从专有数据库当中提取class文件。
- 将java源文件动态编译为.class文件。
类的加载最终的产物是位于堆区当中的Class对象,Class对象封装了类在方法区当中的数据结构,并向Java程序员提供了访问方法区内数据结构的接口。
有两种类型的类加载器:
一:Java虚拟机自带的类加载器。
根类加载器(BootStrap)。
扩展类加载器(Extension)。
系统类加载器(System)。
二:用户自定义的类加载器
java.lang.ClassLoader类的子类。
用户可以定制类的加载方式。
每一个Class对象都保留着对加载它的类加载器的引用,而jvm当中,对BootStap(jvm不允许访问,是c++实现的)的类加载器的引用置位null。看如下例子:
package com.lory.jvm;
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz = Class.forName("java.lang.String");
System.out.println(clazz.getClassLoader());
Class clazz2 = Class.forName("com.lory.jvm.C");
System.out.println(clazz2.getClassLoader());
}
}
class C{
}
输出:null jdk.internal.loader.ClassLoaders$AppClassLoader@726f3b58
JVM规范允许类加载器可以预先加载某个类,如果在加载类的.class文件时文件缺失或者文件存在错误,类加载器必须在程序首次使用该类时才报告这个错误(LinkageError)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类被加载后就进入了连接验证阶段,连接就是将读入到内存当中的二进制数据合并到虚拟机的运行时环境当中。
类的验证主要包含以下内容:
类文件的结构检查,是否符合java规范的固定格式。
语义检查:确保类符合java规定的语法格式。
字节码验证:确保字节码流可以被java虚拟机安全的执行。
二进制兼容的验证:确保相互引用的类之间协调一致。
类的准备阶段,主要是为类的静态变量分配内存,并设置默认的初始值,例如:如下有Class A,在准备阶段,虚拟机将为int类型的静态变量分配4个字节的内存空间,并赋予默认值0,将为long类型的静态变量分配8个字节的内存空间,并赋予默认值0。
public class A{
private static int a;
private static long b;
static{
b=2L;
}
...
}
类的解析阶段:java虚拟机会将类的二进制数据当中的符号引用替换为直接引用,如下:worker类的gotoWork()方法会引用Car类的run方法:
public void gotoWork(){
car.run();//这段代码在worker类的二进制数据当中变现为符号引用。
}
在worker的二进制数据当中,包含了一个对Car类的run方法的符号引用,它由run方法的全名和相关描述符组成,在类的解析阶段,java虚拟机会把这些符号引用替换成一个指针,该指针指向Car类的run方法在方法区的内存位置,这个指针就是直接引用。
类的初始化:该阶段,java虚拟机执行类的初始化语句,为类的静态变量赋予初始值,在程序中,静态变量的初始化有两种途径:1.在静态变量的声明处进行初始化。2.在静态代码块当中进行初始化。
类的初始化步骤:
1.假如这个类还没有进行加载和连接,先进行加载和连接。
2.假如类存在直接父类,并且这个类还没有进行初始化,那么先初始化直接父类。
3.假如类当中存在初始化语句,那么依次执行这些初始化语句。
那么,回到开始,为什么MyTest的执行结果是1,0。首先,在类加载的准备阶段:singleton静态变量初始化默认值为null,count1和count2都被初始化默认值为0.然后,类的初始化阶段加载初始化语句:singleton被赋值一个新的实例,调用构造方法,count1和count2都自增,变为1,然后执行count1和count2的初始化语句,count1无初始化语句,依然是1,而count2被初始化赋值为0,所以最后的结果为1和0.