JVM学习:生命周期

大的来说,当启动一个JAVA程序时,一个JVM即启动了,当程度退出时,JVM也随之消亡。

程序退出指:1. 所有的非daemon线程都终止了 

                    2. 某个线程调用了类Runtime或者System的exit方法


当同时启动多个JAVA程序时,即启动多个JVM,每个JAVA程序都运行于自己的JVM中。

一个JVM表现为一个进程,如我同时启动了3个JAVA程序,有3个javaw的进程

JVM的是通过main()函数作为入口启动的。

类的生命周期

当我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示

JVM学习:生命周期

下面依次讲解

1.   装载:

       类的加载就是把类的.class文件中的二进制数据读入到内存中。把它存放在java运行时数据区方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,并作为方法区中这个类的信息的入口。

       加载来源

       常用的:1.本地加载,根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容

                     2.从jar文件中读取

          还有:1.从网络中获取,比如10年前十分流行的Applet。

                    2.根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。

                    3.从非class文件中获取,其实这与直接从class文件中获取的方式本质上是一样的,这些非class文件在jvm中运行之前会被转换为可被jvm所识别的字节码文件。

       加载方式

       类的加载是由加载器完成的,可分为两种:

       1。 java虚拟机自带的加载器,包括启动类加载器,拓展类加载器和系统类加载器。

       2。 用户自定义的类加载器,是java.lang.ClassLoader类的子类的实例。用户可以通过它来制定类的加载器。

       加载时机

        对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。

        加载阶段是类的生命周期中的第一个阶段,加载阶段之后,是连接阶段。

2.   连接

       这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析有一点需要注意,有时连接阶段并不会等加载阶段完全完成之后才开始,而是交叉进行,可能一个类只加载了一部分之后,连接阶段就已经开始了。但是这两个阶段总的开始时间和完成时间总是固定的:加载阶段总是在连接阶段之前开始,连接阶段总是在加载阶段完成之后完成。

   验证:保证被加载的类有正确的内部结构,并且与其他类协调一致。如果jvm检查到错误,就会抛出相应的Error对象。

       类验证的内容:

        1。 类文件的结构检查,确保类文件遵从java类文件的固定格式。

        2。 语义检查确保本身符号java语言的语法规定,如验证final类型的类没有子类等

        3。 字节码验证:确保字节码可以被jvm安全的执行

        4 。二进制兼容的验证,确保相互引用的之间协调一致,如Worker类的goWork方法会调用Car类的run方法,jvm在验证Worker类时,会检查方法区内是否存在car类的run方法,如不存在会抛出NoSuchMethodError错误。

-----疑问:由java编译器生成的java类的二进制数据肯定是正确的,为什么还要进行类的验证? 因为java虚拟机并不知道某个特定的.class文件到底是如何被创建的,这个.class文件有可能是由正常的java编译器生成的,也可能是由黑客特制的,黑客视图通过它来破坏jvm环境。类的验证能提高程序的健壮性,确保程序被安全的执行。

   准备:为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。注意,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:

       * 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。

       * 引用类型的默认值为null。

       * 常量的默认值为我们程序中设定的值,如我们定义final static int a = 100,则准备阶段中a的初值就是100。

   解析:把常量池中的符号引用转换为直接引用。

             符合引用?直接引用?    在Worker类的gotowork方法中会引用Car类run方法。

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

             在Worker类的二进制数据中,包含了一个对Car类的run方法的的符号引用,它有run方法的全名和相关描述符号组成,在解析阶段jvm会把这个符号引用替换为一个指针,该指针指向Car类的run方法在方法区里的内存地址,这个指针就是直接引用,car.run()就是符合引用。

连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化

3.   初始化:

       如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:

       1. 通过new关键字实例化对象,访问或设置类的静态变量,调用类的静态方法。

       2. 通过反射方式执行以上三种行为。

       3. 初始化子类的时候,会触发父类的初始化。

       4. 作为程序入口直接运行时(也就是直接调用main方法)。

        除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。请看主动引用的示例代码:

import java.lang.reflect.Field;  
import java.lang.reflect.Method;  
  
class InitClass{  
    static {  
        System.out.println("初始化InitClass");  
    }  
    public static String a = null;  
    public static void method(){}  
}  
  
class SubInitClass extends InitClass{}  
  
public class Test1 {  
  
    /** 
     * 主动引用引起类的初始化的第四种情况就是运行Test1的main方法时 
     * 导致Test1初始化,这一点很好理解,就不特别演示了。 
     * 本代码演示了前三种情况,以下代码都会引起InitClass的初始化, 
     * 但由于初始化只会进行一次,运行时请将注解去掉,依次运行查看结果。 
     * @param args 
     * @throws Exception 
     */  
    public static void main(String[] args) throws Exception{  
    //  主动引用引起类的初始化一: new对象、读取或设置类的静态变量、调用类的静态方法。  
    //  new InitClass();  
    //  InitClass.a = "";  
    //  String a = InitClass.a;  
    //  InitClass.method();  
          
    //  主动引用引起类的初始化二:通过反射实例化对象、读取或设置类的静态变量、调用类的静态方法。  
    //  Class cls = InitClass.class;  
    //  cls.newInstance();  
          
    //  Field f = cls.getDeclaredField("a");  
    //  f.get(null);  
    //  f.set(null, "s");  
      
    //  Method md = cls.getDeclaredMethod("method");  
    //  md.invoke(null, null);  
              
    //  主动引用引起类的初始化三:实例化子类,引起父类初始化。  
    //  new SubInitClass();  
  
    }  
}

   上面的程序演示了主动引用触发类的初始化的四种情况。

    在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态代码块,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。

    在初始化静态赋值语句和静态代码块时,是按顺序执行的。如果有父类则先初始化父类的。

    顺序执行指,不管是赋值语句还是代码块,谁在前就先初始化谁

class InitClass{  // 1
    static{  // 2
        System.out.println("运行父类静态代码");  // 3
    }  // 4
    public static Field1 f1 = new Field1();  // 5
    public static Field1 f2;   // 6
}

   上面初始化顺序为 3, 5。 6不会初始化,因为其是声明操作,没赋值。

class InitClass{  // 1
    public static Field1 f1 = new Field1();  // 2
    public static Field1 f2;   // 3
    static{  // 4
        System.out.println("运行父类静态代码");  // 5
    }  // 6
}

   上面初始化顺序为 25。

4.   使用:

       类的使用包括主动引用和被动引用,主动引用在初始化的章节中已经说过了,下面我们主要来说一下被动引用:

        1. 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。

        2. 定义类数组,不会引起类的初始化。

        3. 引用类的常量(final,其在加载的准备阶段就赋值好了),不会引起类的初始化。

        4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动引用,不会导致类的初始化。当程序调用Class类的静态方法forName("ClassA")时,才是对ClassA的主动使用,将导致classA被初始化,他的静态代码块被执行

class InitClass{  
    static {  
        System.out.println("初始化InitClass");  
    }  
    public static String a = null;  
    public final static String b = "b";  
    public final static int c =(int)Math.random();// 非编译时常量
    public static void method(){}  
}  
  
class SubInitClass extends InitClass{  
    static {  
        System.out.println("初始化SubInitClass");  
    }  
}  
  
public class Test4 {  
  
    public static void main(String[] args) throws Exception{  
    //  String a = SubInitClass.a;// 引用父类的静态字段,只会引起父类初始化,而不会引起子类的初始化  
    //  String b = InitClass.b;// 使用类的常量不会引起类的初始化,因为在加载解析时就把InitClass.b解析为常量值了.
    //  int c = InitClass.c; // 此处也是类的常量,但是其在运行期才知道,所以是会触发初始化的。但不管在什么阶段初始化,final标示的变量在初始化后就不会变,如不是final的,每次new都会变
        SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引起类的初始化  
        
        ClassLoader loader = ClassLoader.getSystemClassLoader();// 获得系统的类加载器  
        System.out.println("系统类加载器:" + loader);  
        Class objClass = loader.loadClass("InitClass"); // 此处不会初始化InitClass
        objClass = Class.forName("InitClass");// 此处才会初始化InitClass
    }  
}

   最后总结一下使用阶段:使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。

5.   卸载:

       在类使用完之后,如果满足下面的情况,类就会被卸载:

       1. 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。

       2. 加载该类的ClassLoader已经被回收。

       3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

        如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。


以上是类的整个生命周期。

你可能感兴趣的:(jvm,生命周期)