JVM之加载、连接与初始化

一、JVM与程序生命周期

1、当执行一个Java程序时,操作系统会启动一个JVM进程,进程中有一个主线程会去负责执行程序;当程序执行完毕后,JVM进程也就消亡了,而JVM将会结束生命周期的情况有如下几种:

  • 程序执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止,比如遇到了异常而没有捕获,而是一直往上抛给了JVM
  • 由于操作系统出现错误而导致JVM进程终止

二、类的加载、连接与初始化概览

1、当执行一个Java程序时,会依次进行如下的工作:

  • 加载:查找并加载类的二进制数据
  • 连接:又分为三个阶段,如下:

                     |- 验证:确保被加载的类的正确性

                     |- 准备:为类的静态变量分配内存,并将其初始化为默认值(该数据类型的默认值)

                     |- 解析:把类中的符号引用转换为直接引用 

  • 初始化:为类的静态变量赋予正确的初始值(用户赋予的值)

    整个过程如下图:注意,在这过程中都还没有对象的存在,所以只存在静态变量而不存在实例变量

   

三、加载

1、类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,并向Java程序员提供了访问方法区内的数据结构的接口,所以类的加载的最终产物是位于堆区中的那个Class对象(每个类都有一个唯一的Class对象),这也就是反射的基础。如下图:

2、加载class文件的方式有如下几种:

  • 从本地系统中直接加载
  • 通过网络下载并加载class文件
  • 从zip、jar等归档文件中加载class文件
  • 从专有数据库中提取class文件
  • 将Java源文件动态编译为class文件

3、类加载器执行加载的时间

类加载器不需要等到某个类被“首次主动使用”时再加载它(主动使用在Java程序对类的使用方式中会提到)。JVM规范允许类加载器在预料到某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误),而如果这个类一直都没有被程序主动使用,则类加载器就不会报告错误。

4、在一个生命周期中,一个类只会被加载一次,除非该类被卸载了,才可以被重新加载,或者说被其它类加载器加载。

四、验证

1、类被加载后,就进入了连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。而在连接阶段执行的第一个步骤就是验证,验证的主要内容如下:

  • 类文件的结构检查:确保类文件遵从Java类文件的固定格式
  • 语义检查:确保类本身符合Java语言的语法规定
  • 字节码验证:确保字节码流可以被JVM安全地执行。字节码流代表Java方法(包括静态方法和实例方法),它是由被称做操作码的单字节指令组成的序列,每个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数
  • 二进制兼容性验证:确保相互引用的类之间协调一致。比如在Worker类的gotoWork()方法中会调用Car类的run方法,JVM在验证Worker类时,会检查在方法区是否存在Car类的run方法,假如不存在(当Worker类和Car类的版本不兼容,就会出现此问题),就会抛出NoSuchMethodError错误

五、准备

1、在准备阶段,JVM为类的静态变量分配内存,并设置默认的初始值。如下代码的User类:在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并赋予int类型的默认值0;为long类型的静态变量b分配8个字节的内存空间,且赋予long类型的默认值0,所以在准备阶段时,a和b的值均为0,并不是用户赋予的值1和2。

[java] view plaincopy

  1. public class User {  
  2.     public static int a = 1;  
  3.     public static long b;  
  4.   
  5.     static {  
  6.     b = 2;  
  7.     }  
  8. }  

六、解析

1、在解析阶段,JVM会把类的二进制数据中的符号引用替换为直接引用。如下的代码中,在Worker类的gotoWork()方法中会调用Car类的run()方法,在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它是由run()方法的全名和相关描述符组成。在解析阶段,JVM会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置,这个指针就是直接引用。

[java] view plaincopy

  1. public void gotoWork() {  
  2.     car.run(); //这段代码在Worker类的二进制数据中表示为符号引用  
  3. }  

七、初始化

1、在初始化阶段,JVM执行类的初始化语句,为类的静态变量赋予用户给定的初始值。在程序中,静态变量的初始化有如下两种途径:这两种方式本质上是一样的,JVM会按照初始化语句在类文件中的先后顺序来依次执行它们

  • 在静态变量的声明处进行初始化
  • 在静态代码块中进行初始化

如下代码中,静态变量a和b都被显式初始化,分别为1和2,而静态变量c没有被显示初始化,它将保持默认值0

[java] view plaincopy

  1. public class User {  
  2.     public static int a = 1;  //在静态变量的声明处进行初始化  
  3.     public static long b;  
  4.     public static long c;  
  5.   
  6.     static {  
  7.         b = 2;  //在静态代码块中进行初始化  
  8.     }  
  9. }  

2、初始化步骤

  • 如果这个类还没有被加载和连接,则就先进行加载和连接
  • 如果类存在直接的父类,且这个父类还没有被初始化,那就先初始化直接的父类,如果直接父类还有父类,则继续先初始化直接父类的父类,也就是说当初始化一个子类时,是从顶层父类到子类依次初始化
  • 如果类中存在初始化语句,那就依次执行这些初始化语句

针对第二点的例子如下代码:依次输出的语句为:static block of class Test 、static block of class Parent 、static block of class Child 、2

[java] view plaincopy

  1. class Parent {  
  2.       
  3.     public static int x = 1;  
  4.       
  5.     static {  
  6.         System.out.println("static block of class Parent");  
  7.     }  
  8. }  
  9.   
  10. class Child extends Parent {  
  11.       
  12.     public static int y = 2;  
  13.       
  14.     static {  
  15.         System.out.println("static block of class Child");  
  16.     }  
  17. }  
  18.   
  19.   
  20. public class Test {  
  21.       
  22.     static {  
  23.         System.out.println("static block of class Test");  
  24.     }  
  25.       
  26.     public static void main(String[] args) {  
  27.         System.out.println(Child.y);  
  28.     }  
  29.   
  30. }  

八、初始化时机

1、Java程序对类的使用方式分为两种,分别是主动使用和被动使用。所有的JVM实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们(就是指上面的“七、初始化”,所以初始化是有条件的)。而主动使用包括六种,如下:

  • 创建类的实例,比如new User(),是对User类的主动使用
  • 访问某个类或接口的静态变量,或对该静态变量赋值,比如int b = User.a或User.a = b都是对User类的主动使用
  • 调用类的静态方法,比如User.show()是对User类的主动使用
  • 反射,比如Class.forName("com.classloader.User")是对User类的主动使用
  • 初始化一个类的子类,比如下面的代码就是对User类的主动使用

[java] view plaincopy

  1. class User {}  
  2.   
  3. class Child extends User {  
  4.     public static int a = 3;  
  5. }  
  6.   
  7. .....  
  8.   
  9. Child.a = 10;  
  • JVM启动时被标明为启动类的类,比如执行命令"java User"也是对User类的主动使用

而除了以上的六种主动使用外,其他使用Java类的方式都是被看作是对类的被动使用,不会导致类的初始化。

2、编译时常量

如下代码所示,结果仅仅会输出1;而如果把语句“public static final int x = 1”改为“public static final int x = new Random().nextInt(100)”,则会首先输出“static block of class Sample”,然后输出一个随机数字。这是因为语句“public static final int x = 1”在编译时就能确定x的值,x属于编译时常量;而语句“public static final int x = new Random().nextInt(100)”在编译时是不能确定x的值的,只有在运行时才能确定x的值,所以它是一个运行时常量。如果是编译时常量,程序在访问“Sample.x”时不算是对Sample类的主动使用,所以不会导致对Sample类的初始化,所以仅仅会输出1;而如果是运行时常量,当程序在访问“Sample.x”时是对Sample类的主动使用,所以会导致对Sample类的初始化,所以会输出“static block of class Sample”。

[java] view plaincopy

  1. class Sample {  
  2.       
  3.     public static final int x = 1;  
  4.       
  5.     static {  
  6.         System.out.println("static block of class Sample");  
  7.     }  
  8. }  
  9.   
  10.   
  11. public class Test {  
  12.       
  13.     public static void main(String[] args) {  
  14.         System.out.println(Sample.x);  
  15.     }  
  16.   
  17. }  

3、接口的初始化

当JVM初始化一个类时,要求它的所有父类都要已经被初始化(七、初始化中的初始化步骤第二点有介绍过),但是这条规则并不适合于接口,如下:

  • 在初始化一个类时,并不会先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化它的父接口

所以,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。

4、只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。如下代码:程序会依次输出“static block of class Parent”、 1 、“static method show of class Parent”,从结果中可以父类Parent初始化了,而子类Child却没有被初始化,因为静态变量x和静态方法show都是定义在父类Parent中的,子类只是继承了下来,所以不认为是对子类的主动使用,会认为是对定义a和show的类的主动使用,所以对Parent类进行了初始化。

[java] view plaincopy

  1. class Parent {  
  2.       
  3.     public static int x = 1;  
  4.       
  5.     static {  
  6.         System.out.println("static block of class Parent");  
  7.     }  
  8.       
  9.     public static void show() {  
  10.         System.out.println("static method show of class Parent");  
  11.     }  
  12. }  
  13.   
  14. class Child extends Parent {  
  15.       
  16.     static {  
  17.         System.out.println("static block of class Child");  
  18.     }  
  19. }  
  20.   
  21. public class Test {  
  22.   
  23.     public static void main(String[] args) {  
  24.         System.out.println(Child.x);  
  25.         Child.show();  
  26.     }  
  27.   
  28. }  

之前有说过程序中对子类的初始化会导致父类被初始化。但是从以上结果中还可以发现,对父类的初始化并不会导致子类的初始化,可以想象一下,不可能说生成一个Object类的对象就要导致系统中所有的子类都被初始化吧。

5、调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

6、在一个类加载器中,如果一个类已经被初始化了,那么就不会再进行第二次初始化。如下代码:语句“Parent p = new Parent()”是对Parent类的首次主动使用,所以会对Parent类进行初始化,所以会首先输出“static block of class Parent”,接着执行代码“System.out.println(Child.y)”,这是对子类Child的首次主动使用,所以会初始化子类Child,但是由于它有直接父类Parent,所以应该要先初始化父类,但是由于其父类已经初始化过了,所以不会再进行父类的初始化,所以接着初始化子类Child,输出“static block of class Child”,但是输出y的值2。

[java] view plaincopy

  1. class Parent {  
  2.       
  3.     public static int x = 1;  
  4.       
  5.     static {  
  6.         System.out.println("static block of class Parent");  
  7.     }  
  8. }  
  9.   
  10. class Child extends Parent {  
  11.       
  12.     public static int y = 2;  
  13.       
  14.     static {  
  15.         System.out.println("static block of class Child");  
  16.     }  
  17. }  
  18.   
  19. public class Test {  
  20.   
  21.     public static void main(String[] args) {  
  22.         Parent p = new Parent();  
  23.         System.out.println(Child.y);  
  24.     }  
  25.   
  26. }  

九、程序分析

1、如下面的代码,当运行该程序(java Test)时,会首先加载Test类,然后执行验证,接着执行准备,由于Test类中没有任何静态变量,所以这一步好像没做什么,然后接着执行解析;因为Test类是启动类,当执行java Test时是对Test类的首次主动使用,所以会对Test类进行初始化;我们假设ClassLoaderDemo此时也已经被加载进来了,而且执行了验证、准备、解析,所以此时ClassLoaderDemo的静态变量a的值为0,b的值也为0,instance的值为null。由于在Test类的主方法中访问了ClassLoaderDemo类的静态变量,是对ClassLoaderDemo首次主动使用,所以会对ClassLoaderDemo类进行初始化,从上到下执行,首先a没有显示初始化,保持为0,b经过了显示初始化,但是还是为0,instance这时通过new关键字进行对象实例化,是对ClassLoaderDemo的主动使用,但是由于不是首次,所以不会再对ClassLoaderDemo进行初始化,所以经过对象实例化后a和b都进行了自增,所以在打印时会输入1和1。但是如果把注释掉的那行语句给打开,而注释掉代码的第六行,当对ClassLoaderDemo类进行初始化时,首先是进行对象的实例化(还是不是首次主动使用,所以不会再对ClassLoaderDemo进行初始化),此时a和b都变成了1,接着a初始化,但是没有显示初始化,所以保持自增后的值1,最后初始化b,b进行了显示初始化,所以b的值由自增后的1变成了0,所以最终会输出1和0。

[java] view plaincopy

  1. class ClassLoaderDemo {  
  2.   
  3.     //private static ClassLoaderDemo instance = new ClassLoaderDemo();  
  4.     public static int a;  
  5.     public static int b = 0;  
  6.     private static ClassLoaderDemo instance = new ClassLoaderDemo();  
  7.       
  8.     public ClassLoaderDemo() {  
  9.         a++;  
  10.         b++;  
  11.     }  
  12.       
  13. }  
  14.   
  15. public class Test {  
  16.     public static void main(String[] args) {  
  17.         System.out.println(ClassLoaderDemo.a);  
  18.         System.out.println(ClassLoaderDemo.b);  
  19.     }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值