谁无暴风劲雨时,守得云开见月明
JVM的运行机制
JVM(java virtual machine) 是用于运行JAVA字节的虚拟机。由一套字节码指令集,一组程序寄存器,一个虚拟栈,一个虚拟堆,一个方法区和一个垃圾回收器组成。
JVM源文件在通过编译后变为相应的.class文件(字节码文件),字节码文件再根据不同的解释器变为机器码文件调用操作系统上的本地方法区。这也就是为什么JAVA能够跨平台的原因。
在一个java进程开始,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例。进程退出或关闭,则虚拟机实例就消亡。多个虚拟机之间相互独立不能通行。
java程序的运行过程如下:
1)Java源文件被编译器编译成字节码文件。
2)JVM将字节码文件编译成相应操作系统的机器码。
3)机器码调用本地方法区。
JVM的类的加载阶段
多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启动程序,这里首先需要通过类加载器把主类加载到JVM,主类在运行过程中如果使用到其它类,会逐步加载这些类。
注意:对于我们自己写的jar包里的类不是一次性全部加载的,是使用到时才加载。
JVM的类的加载分为5个阶段:
加载=>验证=>准备=>解析=>初始化
1)加载: 将.class字节码文件读取到运行时区域的方法区内,在堆中创建java.lang.class文件,并封装类在方法的数据结构过程
注意:使用到类时才会加载,例如调用类的main()方法,new对象等。
2)验证: 验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全(验证字节码的正确性)。
3)准备: 为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。
注意:这里final类型的变量在准备阶段的数据初始化过程不同。比如一个成员变量的定义如下:
public static long value = 1000;
上面代码,静态变量value在准备阶段的初始值是为0,将1000值设置为1000是在对象初始化时完成。如果将value声明为final类型。
public static final long value = 1000;
则JVM在编译阶段为final类型的变量value生成对应的ConstantValue属性,虚拟机在准备阶段根据ConstantValue值赋值给value。
4)解析: 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用(将类的二进制数据中的符号引用换为直接引用)。
5)初始化: 通过构造器的client
方法将类进行初始化,client
方法是在编译阶段自动收集类中静态语句和变量赋值操作组成。
JVM规定:只有在父类的client
方法初始化完成后,子类的方法才可以被执行,原因:子类要构造完成要调用父类的东西,父类还没有构造怎么调用呢?一个类中没有静态语句和静态语句快的时候,编译器不会为该类生成client
深入理解加载和初始化
引出问题
JVM主要完成三件事:
(1)编译: 通过包名类名去定义此类的二进制字节流(Class文件)
(2)存入方法区: 将准备好的二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)生成class类对象: 在内存中生成一个代表这个类的java.lang.Class对象。这个对象不一定在堆中也可能在方法区中。
图解过程:
下面用代码代表一下类的引用。
public class TestDynamicLoad {
static {
System.out.println("**********static code *******");
}
/**
* 运行配置VM options : -verbose:class
* @param args
*/
public static void main(String[] args) {
new A();
System.out.println("********** load main ***********");
//会被加载,也会被初始化
B b = new B();
// 不会被加载,也不会被初始化.
//B b = null;
// 会被加载,但是不会被初始化.
//B[] arr = new B[10];
}
}
问题:
我们看见B类的static字段能够调用,但是却没有走构造函数,那么类是加载了的,但是类对象的赋值是在什么阶段呢?
类的初始化
类初始化是类加载的最后一步。
Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:
(1)使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。
(2)通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。
(3)当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。
(4)当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。
(5)使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果。
注意,虚拟机规范使用了“有且只有”这个词描述,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用。
被动引用
public class Test{
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
//父类
class SuperClass {
//静态变量value
public static int value = 666;
static {
System.out.println("父类初始化!");
}
}
//子类
class SubClass extends SuperClass {
static {
System.out.println("子类初始化!");
}
}
输出:
通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化。
数组引用类
通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”情况,属于“被动引用”
静态常量引用类
静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类。因此初始化不会赋值。
类加载器
JVM提供了4种类加载器,分别是启动类加载器,扩展类加载器和应用程序类加载器。
1)启动类加载,负责加载JAVA_HOME//lib中的类库,通过参数-Xbootclasspath
参数指定虚拟机认可的路径。
2)扩展类加载器,加载JAVA_HOME/lib/ext目录中的类库
3) 引用程序类加载器,负责加载用户路径上的类库
4)自定义加载器(CustomClassLoader):负责加载用户自定义路径下的类包。
验证启动的时候加载了核心类,加一个属性:VM options:-verbose:class
双亲委派机制
JVM 通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后,自己不会去尝试加载这个类,而是吧这个类加载请求向上委派给父类进行加载,父类收到这个类请求后会将请求委派给自己的父类,以此类推,这样所有类的加载请求都会加载到启动类加载器中。倘若父类加载不到就会向下请求给自己的子类。找到该类加载被成功。若找不到该类,则JVM会抛出ClassNotFound异常。
加载流程如下:
双亲委派机制的核心目的就是保证类的唯一性和安全性。例如:在加载jar包中的object类,无论那个类有这个请求都会委派给父类请求,父类请求不到了再向下请求,这样就保证了类的唯一性。