白话JVM之虚拟机类加载机制

白话JVM之虚拟机类加载机制_第1张图片上图中,加载、验证、准备、初始化和卸载这个五个阶段的必须是按部就班的,而解析在某些情况下可以在初始化阶段以后再开始。

《Java虚拟机规范》只规范了在什么时候初始化,其他的没规范,但是他又规范了加载、验证、准备是要在初始化之前进行。规定有以下场景才会强制初始化(有且只有):

  • 遇到new、getstatic、pustatic、或invokestatic这四条字节码指令时,如果类型没有进行初始化,就要先进行初始化,能够生成这四条指令的典型的Java代码场景有:
    *使用new关键字实例化对象的时候
    *读取或设置一个类型的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候
    *调用一个类型的静态方法的时候
  • 使用java,lang,reflect包的方法对类型进行反射调用的时候,也必须先进行初始化。
  • 虚拟机启动的时候,main方法那个类,
  • jdk7新加入了动态语言支持,如果一个java.lang.invoke.MethodHandle实力最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
  • 当一个接口定义了jdk8新加入default关键字修饰的接口方法,如果有这个接口的实现类发生了初始化,那接口要在其之前被初始化。

出现以上六种称为主动引用,除此之外,所有的引用类型都不会触发初始化,称为被动引用。举个例子

public class SuperClass {
     
    static {
     
        System.out.println("Superclass init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass {
     
    static {
     
        System.out.println("subclass init");
    }
}
class NotInitialization{
     
    public static void main(String[] args) {
     
        System.out.println(SuperClass.value);
    }
}

输出:Superclass init
	 123

上面这个就是,非直接引用子类的静态字段,但是直接引用了父类的静态字段,所以只会打印Superclass init和123

public class SuperClass {
     
    static {
     
        System.out.println("Superclass init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass {
     
    static {
     
        System.out.println("subclass init");
    }
}
class NotInitialization{
     
    public static void main(String[] args) {
     
        SuperClass[] sca = new SuperClass[10];
    }
}
输出:

上面代码啥也没输出。数组在jvm中不直接引用原类型,字节码指令anewarray会生成一个类,直接继承于java.lang.Object,java语言对数组的操作比c++安全,这个机制影响很大。

public class SuperClass {
     
    static {
     
        System.out.println("Superclass init");
    }
    public final static int value = 123;
}

class SubClass extends SuperClass {
     
    static {
     
        System.out.println("subclass init");
    }
}
class NotInitialization{
     
    public static void main(String[] args) {
     
        System.out.println(SuperClass.value);
    }
}
输出:123

当value被finali修饰的时候,就不输出Superclass init,这是因为在编译优化的时候,value已经被放到NotInitialization的常量池中了,调用的时候是对自身常量池的引用,也就是说,NotInitialization的class文件中没有对SuperClass类的符号引号入口,这两个类在编译完成后,就彼此无关了。

加载

加载阶段虚拟机需要完成三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(一般找到类在磁盘的地址并且转化为流,但是也可能代理生成的,也可能是jsp)
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的对象,并且作为方法区这个类的各种数据的访问入口。

数组不太一样,数组是由java虚拟机直接在内存中动态构造出来的。

验证

  1. 文件格式验证,验证calss文件是不是符合标准
  2. 元数据验证就是在idea上编程的时候经常提示的红线
  3. 字节码验证,最复杂的一个阶段,通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的,由于这个错误基本也会被编译器拦截,而且到了生产环境后,代码基本是完整的,为了提高启动速度,在生产环境上可以用-XX:UserSplitVerifier来关闭这个校验,
  4. 符号引用验证,发生在虚拟机符号引用将要转化为直接引用的时候,这个转化发生在解析阶段,所以,类加载的步骤是可以重叠的,但是开始顺序严格执行。

准备

给被static修饰的变量分配内存,并且设置初始值的阶段,jdk7之前用永久代来实现方法去,就放在永久代,jdk7以后,和对象一起放在堆中。而被final修饰过的,则会被虚拟机修饰额ConstantValue属性,会在准备阶段就会被赋值
public static int value = 123;在准备阶段值是0;
public static final int value = 123;在准备阶段值是123;

数据类型 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

解析

符号引用:由于引用对象可能还未加载到虚拟机了,没有内存地址,只能一组符号表示目标的定位。
直接引用:直接引用是可以直接指向目标指针、相对偏移量或者是一个能间接定位到目标的句柄,如果是直接引用,那么被引用的一定是在虚拟机中真实存在的。

解析就是把符号引用转化为直接引用的过程。同时会对被引用都对象、方法、属性进行权限判断。

初始化

初始化是类加载的最后一个步骤,这个时间才开始执行我们自己写的代码。主导权移交给程序本身,即执行< clinit>()方法的过程,给准备阶段赋值为零值的那些属性赋予具体的值。

clinit

< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的 语句合并产生的。
白话JVM之虚拟机类加载机制_第2张图片由于静态语句块中是可以互相访问的,所以先赋值后定义是可以的,但是其他调用会报非法的向前引用的错误。

public  class Test {
     
    public static void main(String[] args) {
     
        System.out.println(Sub.B);
    }
    static class Parent {
     
        public static int A = 1;
        static {
     
            A = 2;
        }
    }
    static class Sub extends Parent{
     
        public static int B = A;
    }
}
输出:2

上图表示,父类的< clinit>一定优先于子类的执行,所以java中第一个被执行的< clinit>一定是java.lang.Object

类加载器

类+类加载器才可以确定唯一性,如果同一个类由两个不同的类加载器加载,则必不相等。

双亲委派模型

从java虚拟机角度来看,只存在两种类加载器,一种是Bootstrap ClassLoader(启动类加载器)由C++实现,另一种是其他所有的类加载器,都是由java语言实现,全部继承自抽象类java.lang.ClassLoader

  • 启动类加载器(Bootstrap Class Loader)负责加载存放在lib目录下的,或者被-Xbootclasspath参数执行的路径而且被java虚拟机所能识别的。
  • 扩展类加载器(Extension Class Loader)用于加载lib\ext目录下的。
  • 应用程序类加载器(Application Class Loader)用于加载ClassPath下的所有的类库。一般情况下是程序中默认的加载器.

双亲委派模型就是类加载时,子加载器调用父类的加载方法loadClass,父类未找到反过来调用子类的loadClass去加载。

你可能感兴趣的:(小白学JVM,java,jvm)