彻底搞懂JVM从启动到结束的过程

JVM从启动到结束会经历什么?

1、Load the Class(加载类)

​ 对于一个Java类,首先会将其编译成class文件。虚拟机在执行某个方法时发现内存中没有这个类的二进制表示,那么就会通过类加载器(class loader)来尝试找到找到这个类的二进制表示

双亲委派机制

​ 给一个相同的类名,总是能够加载到相同的类,这是通过双亲委派机来实现的,每个类加载器都有各自的加载策略,HotSpot内置了几种类加载器,他们进行类加载时的目录时不一样的,当类加载器加载一个类的时候会先将这个类的加载任务交给它的父类加载器,层层委托,如果父类加载器能加载,那么就加载,如果父类加载不了,则会交给子类来加载 。说白了,在加载一个类的时候,父类加载器的优先级要大于子类。比如说,你自己写了一个String类型和Java自带的String类型重名了,然后程序在启动的时候肯定会将String类型加载到内存中去,当进行类加载的时候会优将加载String类型的任务交给父类,说白了就是Java自带的类加载器,他会加载他自己的String类型,而不是你自己写的。

2、Link(链接)

此过程可分为三步:验证、准备和解析(可选)

验证

​ 这一步会检查加载的类的格式是否正确,是否有正确格式的符号表以及语义是否正确

准备

​ 准备工作包括静态存储与虚拟机内部使用的数据结构的空间的分配

解析(可选)

​ 解析阶段的主要内容是加载当前类涉及到的其他类和接口,同时检查引用是否正确,这是一个递归的过程,着类似于C语言的“静态”链接,与之相反的,还有一个比较“懒”的链接方式——只有在类或接口被使用的时候才会对其进行解析。

​ 上面两种方式各有优缺点,第一种方式在运行前就可以发现其中的问题,而第二种可能在运行时才能发现。

3、Initialize (初始化)

初始化一个类的前提是,它的父类和超类都必须要先被初始化,这是一个递归的过程。最简单的Object是所有类的超类,因此,这种递归的初始化到Object就会结束

初始化锁

​ 因为Java是多线程的,所以在加载时为了防止冲突的发生,每一个类或者接口都有一个唯一的初始化锁,并且都有一个状态,用于表示这个类的初始化情况,一共有如下几种:

  • 已经验证并准备好,还未初始化
  • 正在被某个线程初始化
  • 已经被初始化完成,可以被使用
  • 处于错误状态,可能是初始化失败了

当线程尝试对一个类进行初始化的时候,会先去获取初始化锁,在获取到之后,就会根据其具体的状态来决定要不要释放这个锁,以及要不要进行等待。比如:这个类正在被另一个线程初始化,那么当前线程就会释放初始化锁并进入到阻塞状态,知道被正在进行初始化的线程通知已经完成了初始化。

4、Creation of New Class Instances(创建新的实例)

显示实例化和隐式实例化

显示实例化:

​ 这个比较简单,其实就是通过构造函数进行实例化

隐式实例化:

​ 1、加载包含String类型的类或接口会隐式的创建一个String对象

​ 2、装箱操作,如返回类型是Integer,但是方法最后return 0,会隐式的创建一个包装类

​ 3、字符串 + String类型操作会创建一个新的String对象

​ 4、lambda表达式可能会创建实现接口的实例对象

实例化过程

​ 实例化过程中会调用类的构造函数,其中的处理过程一共有五步,因为比较复杂,所以这里只简单提一下,然后通过例子让大家能够理解。

​ 1、执行构造方法时,如果参数也是一个类,那么会执行它的构造方法。比如参数是String类型的,那么会最先会执行String的构造方法。

​ 2、在执行构造方法中具体的逻辑前会先先执行父类的构造方法,这是一个递归的过程,也就是说隐藏了一个super()在构造函数最前面(除了Object)。

​ 3、在调用父类构造方法时,在调用某个被子类重写的方法时,会优先调用子类的(下面会举例子)

例一:

class Point {
    
    
    // super(); //隐藏的父类构造方法
    int x, y;
    Point() {
    
     x = 1; y = 1; }
}
class ColoredPoint extends Point {
    
    
    // super(); //隐藏的父类构造方法
    int color = 0xFF00FF;
}
class Test {
    
    
    public static void main(String[] args) {
    
    
        ColoredPoint cp = new ColoredPoint();
        System.out.println(cp.color);
    }
}

执行流程

ColoredPoint() --> Point() --> Object() --> 初始化x,y --> Point() --> 初始化color变量

例二:

class Super {
    
    
    Super() {
    
     printThree(); }
    void printThree() {
    
     System.out.println("three"); }
}
class Test extends Super {
    
    
    int three = (int)Math.PI;  // That is, 3
    void printThree() {
    
     System.out.println(three); }

    public static void main(String[] args) {
    
    
        Test t = new Test();
        t.printThree();
    }
}

输出:

0
3

执行流程:

1、执行Test()
2、执行Test()中的super(),也就是Super()方法
3、然后执行Super()方法中的super(),也就是Object的构造方法
4、执行Super类的printThree()方法,因为被子类重写了,所以会调用子类的printThree()方法,输出tree变量的值0(是默认值,因为 int three = (int)Math.PI 还没被执行)
5、执行Test类中的 int three = (int)Math.PI 让three的值变为了3
6、再执行Test类中的printThree()方法,此时输出3

5、Finalization of Class Instances(类实例的终结)

​ Object有一个protect修饰的finalize方法,这意味着任何子类都可以调用父类的finalize方法。当一个对象不可达时(不再被使用时)就会被回收,在此之前,Java虚拟机将会调用这个方法。

特点

​ 1、 finalize方法可以释放JVM无法释放的资源,因此仅仅回收对象使用的内存并不能回收它所拥有的所有资源

​ 2、和构造函数不同,finalize的调用是没有顺序的,换句话说就是当一批对象要被回收之前,可以以任何顺序调用他们的finalize方法,甚至可以多个线程并发的调用。

​ 3、与构造函数不同的是,JVM不会自动的调用父类的finalize方法,除非程序中显示的写了

​ 4、如果finalize方法中抛出未捕获的异常,则会忽略该异常并终止对finalize方法的调用,例子如下:

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        testFinalize();
    }

    private static void testFinalize() throws InterruptedException {
    
    
        List<ObjectA> list = new ArrayList<>(10);
        for (int i = 0; i < 5; i++) {
    
    
            list.add(new ObjectA());
        }
        list = new ArrayList<>();
        System.gc();
        Thread.sleep(Long.parseLong("4000"));
        System.out.println("complete!");
    }
}
class ObjectA {
    
    
    @Override
    protected void finalize() {
    
    
        System.out.println("finalize() start");
        // 执行到这里后,因为异常将不会往下执行
        int a = 10 / 0;
        System.out.println("finalize() end");
    }
}

输出:

finalize() start
finalize() start
finalize() start
finalize() start
finalize() start
complete!

6、Unloading of Classes and Interfaces(卸载类和接口)

​ 除了上面说的回收对象实例之外,Java还可以回收类和接口,也就是“卸载”类和接口。类的卸载并不是必须的,这仅仅是一种优化,只有对于一开始加载大量类然后过一段时间后停止使用的应用程序才有意义。

特点

​ 1、当且仅当类加载器被回收时,才能够卸载它所加载的类和接口

​ 2、BootStrap Loader(引导类加载器)加载的类和接口不能被卸载

7、Program Exit(程序退出)

程序退出发生在以下两种情况:

1、所有非守护线程都终止了

2、调用类Runtime.getRuntime().exit(n)或者System.exit(n)方法

猜你喜欢

转载自blog.csdn.net/weixin_44829930/article/details/121192093