初探JVM系列(三)虚拟机类加载机制

初探JVM系列博客

主要内容:

  • JVM加载机制
  • 类加载时机
  • 类加载生命周期
1 .JVM加载机制:
        Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。

2.类加载时机


    2.1主动引用:

  1. 创建类的实例(new)、读取静态变量、对该静态变量赋值(被fianl修饰的的在编译期把结果放入常量池的静态字段除外);
  2. 使用反射(Class.forName())调用的时候;
  3. 初始化一个子类的时候,首先初始化其父类;(接口在初始化的时候并不是要求父接口全部都完成初始化,只有在真正使用到父接口的时候)(如引用接口中定义的常量)才会初始化) 
  4. JVM启动时被指定要执行一个主类(包含main()方法的类),首先首先初始化这个类
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则需要先触发其初始化(不是很理解)
 2.2被动引用:
  1. 通过子类引用父类的静态字段,不会导致子类初始化
  2. 通过数组定义来引用类,不会触发此类初始化
  3. 被fianl修饰的常量会在编译期把结果放入常量池,本质没有引用定义常量的类,因此不会发生初始化
案例如下:
/**
 * @author zpoison
 * @data 2018/5/23/023 11:27
 * 通过子类引用父类的静态字段,不会导致子类初始化
 */
public class SuperClass {
    static {
        System.out.println("SuperClass init");
    }
    public static int value =10;
    public static final String hellow ="hellowworde";

}

/**
 * @author zpoison
 * @data 2018/5/23/023 11:27
 */
public class SupClass extends SuperClass {
    static {
        System.out.println("SupClass init");
    }
}

/**
 * @author zpoison
 * @data 2018/5/23/023 11:26
 */
public class NotInitialization {
    public static void main(String[] args) {
        // 1通过子类引用父类的静态字段,不会导致子类初始化
        System.out.println(SupClass.value);
        //2通过数组定义来引用类,不会触发此类初始化
        SuperClass[] sca = new SuperClass[10];
        //被fianl修饰的常量会在编译期把结果放入常量池,本质没有引用定义常量的类,因此不会发生初始化
        System.out.println(SupClass.hellow);

    }
}
输出结果:
SuperClass init
10
hellowworde

3类加载生命周期

 


3.1加载:

  1. 通过一个类的全限定名称来获取定义此类的二进制字节流。 
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 
  3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
  4. 加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象,(并没有明确规定是在java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,虽然是对象但是存放在方法区内)这个对象成为程序访问方法区中的这些类型数据的外部接口

3.2验证:
  1. 文件格式的验证(魔数、版本、编码等)
  2. 元数据验证(继承关系、重载参数)
  3. 字节码验证(指令跳转、类型转换)
  4.  符号引用验证(常量池中各种符号的引用)(字符串描述的全限定名找到的类和对应的方法与字段和其中的访问权限)
3.3准备:类变量赋值过程;


3.4解析:

  1. 常量池内的符号引用替换为直接引用的过程。
  • 符号引用:符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。 
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
    2.类或接口、字段、类方法、接口方法的解析。


3.5初始化


  1. <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。 
  2. <clinit>()方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个执行的<clinit>()方法的类一定是java.lang.Object。 
  3. 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。 
  4. <clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。 
  5. 接口中可能会有变量赋值操作,因此接口也会生成<clinit>()方法。但是接口与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。 
  6. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那么就可能造成多个进程阻塞


扫描二维码关注公众号,回复: 1508730 查看本文章

参考:《深入理解java虚拟机第二版

猜你喜欢

转载自blog.csdn.net/zpoison/article/details/80421045