本章内容是对《深入理解Java虚拟机:JVM高级特性和最佳实践》的理解和概括。

前言

这是我CVTE面试时候的一个坎儿,因为面试官当时问我的时候,我毫不犹豫的回答了没有接触过这一块的知识。所以之后会从网上挑一些经典的面试题做总结。

类的加载机制

先使用一张图整个加载机制所包含的过程。

image

通过这张图我们可以了解到,整个过程的流程了。下面主要介绍最主要的前5个部分:

加载

需要完成以下三项任务:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
往简单了说,就是读取数据,并对数据的形式做一个转化,变成一个JVM能够认识的模样,然后对应的JVM的内存空间。(注意这个时候,还没有真正的把类塞进去!!)

验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

为什么需要这么一个环节呢? Class文件的产生,并不是一定来自Java源码。他甚至可以由我们直接编写而成,验证能帮我过滤掉错误的Class文件,保障虚拟机的正确运行。

需要完成以下四项任务:
(1)文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

(2)元数据验证:对类的元数据信息中的数据类型等进行校验。
(3)字节码验证:对类的方法体进行校验。
(4)符号引用验证:动作的正确执行。

准备

正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这个阶段存在一个思考。

public class Bean {
    public static int i_static = 123;
    public int i = 123;
}

为什么我们可以直接从main()函数中调用到的i_static,而调用不到i呢?
读者肯定会说,这不是废话吗,i_static是用static修饰的,当然可以调用。但是这是从使用的角度来思考了。
其实这就是准备阶段要干的事情了,在这个阶段,虚拟机已经为这些数据做好了存放的工作,所以我们能够调用。但是i这个变量,在你没有实例化之前,他是没有被存放在内存空间的,自然也就不能够调用了。更直白的说,就是你找不到呗,找不到我怎么用。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

解析是一个不定时的工作内容,因为像new,数组引用这些都是一个视情况而定的事件。

初始化

在书中很明确的提及到以下五种情况,是需要立即对类进行初始化的,或者说只有这五种情况下是需要对一个类进行主动引用:
(1)使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
(2)对类进行反射调用的时候。
(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4)当虚拟机启动时,虚拟机会先初始化带main()的主类。
(5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄”“并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。”

对于静态变量和静态块的加载,是按照在代码中的顺序来进行初始化的。

public class Bean {
    // public static int i_static = 123;
    static {
        i_static = 0;
    }
    public static int i_static = 123;
}

使用这个Bean类进行打印i_static的时候,出现的先后顺序,通过打印就能知道区别了。

优先顺序如下:

  • 静态 → 实例;
  • 父类 → 子类

实践测试代码

// Bean类
public class Bean {
    static {
        System.out.println("Bean static load");
    }
    Bean(){
        System.out.println("Bean load");
    }
}

// 继承自Bean的子类
public class BeanSon extends Bean {
    static {
        System.out.println("BeanSon static load");
    }

    BeanSon(){
        System.out.println("BeanSon load");
    }
}

// 具体调用
public class Main {
    public static void main(String[] args) {
        Bean bean = new BeanSon();
    }
}

另外一个是我在牛客练习时知道的知识,叫做左编译右运行,其实是向上转型的概念,但是这种记法更生动形象。
直接用代码来验证这句话,现在将子类和父类修改成以下形式。

// BeanSon
public class BeanSon extends Bean {
    static {
        System.out.println("BeanSon static load");
    }

    BeanSon() {
        System.out.println("BeanSon load");
    }

    @Override
    void commonHas() {
        System.out.println("commonHas BeanSon");
    }

    void doSomething() {
        System.out.println("doSomething");
    }
}

// Bean
public class Bean {
    static {
        System.out.println("Bean static load");
    }
    Bean(){
        System.out.println("Bean load");
    }

    void commonHas(){
        System.out.println("commonHas Bean");
    }
}

然后使用上面的Main类中的对象bean去调用这个函数,会出现什么情况?

找不到doSomething()这个函数?这就是左编译的意思了,虽然是按照右边的子类运行,但是是不会将子类多出来的方法加入到方法区。
再调用上图中的commonHas()方法后,你又会发现打印的结果是这样的。

它运行出了子类的结果,这也就是右运行的意思了。
以上就是面试一个知识点了,明天继续,fire!!

如果有什么我没有思考到的地方或是文章内存在错误,欢迎与我分享。