乐趣区

关于jvm:深入理解jvm-类加载过程

深刻了解 jvm – 类加载过程

前言

​ 在最早的文章中,咱们尽管探讨过了类加载器的过程,然而并没有讲述外部的细节,本文将会依据类加载器的过程,具体说一下整个类加载的过程中每一个步骤都干什么事件。

​ 类加载的过程如下:加载,验证,筹备,初始化,解析,应用,卸载。重点须要关注的步骤是后面的五个步骤,这些细节算是八股文的内容,所以这篇文章以简略的总结和演绎为主。

概述

​ 本篇次要讲述类加载的加载过程,在类加载的过程当中蕴含了前五个步骤和具体的细节。

类加载的过程

​ 下们拆分这五个步骤,讲讲每一个步骤都做了哪些事件:

加载

​ 第一步是加载,加载做的事件就是什么时候 jvm 须要去找到.class 这个对象,咱们都晓得.class 对象如果办法区中存在的话,java 是不会去加载第二次的,那么类什么时候会进行初始化呢?

​ 咱们最容易想到的就是 new 的时候,所以能够必定在 new 的时候会作为触发条件。接着咱们有时候应用 public static String mind = "xxx" 这种常量的时候,有时候会构建常量类并且间接援用,这时候必定也是须要先把对应的类加载过去的时候才能够应用的,最初既然咱们应用其余类的动态字段会触发,那么应用其余类的静态方法必定也是会触发类加载条件的。下面这三个条件,是咱们最容易想到的三个,上面会略微简单一点点点触发加载动作的条件。

​ 除了 new 之外,咱们还晓得一种形式是通过 java 的 反射 机制,其实就是拿到.class 文件对应的类加载器去生成一个类,反射工具就是来简化这一个动作的,所以这里能够猜到,如果反射须要加载的类还不在办法,那必定也要先把要加载的类加载进来才行。

​ 咱们从继承和实现两个角度去思考什么时候会加载,从继承的角度看,如果父类没有被加载,那么父类也是要被加载进来的,至于为什么必须应用父类,这个问题类结构器能够作为解答,咱们都晓得在结构器的办法会执行一条 super() 的隐式办法,至于为什么要执行 super() 则是因为所有的类的父类都是Object。也是因为 jvm 的类加载器的设计所决定,双亲委派机制决定了所有的子类加载前须要加载父类。(留神,仅仅是加载,是否须要初始化下文会提到)

​ 最初咱们再来看下因为 jdk 版本带来的改良。首先是 jdk7 动静语言的反对,所有波及 new 或者应用动态属性指令的类都会触发加载。而 jdk8 因为引入了接口的 default 办法(默认办法)让接口也能够实现“抽象类”的事件,所以如果有子类实现了领有默认办法的接口,也是须要进行加载的。

​ 上面咱们总结下面对于加载的“初始化”条件:

  • New、动态字段援用、静态方法援用
  • 继承的父类,如果应用的是父类定义的字段或者办法时候会加载父类,然而 不会加载子类。然而如果是然而如果是调用子类的,父类肯定会被加载。
  • 反射机制生成的类须要加载(否则无奈进行反射)。
  • jdk7 动静语言波及 new 和 static 的相干指令
  • jdk8 实现了带有默认办法的接口的类。

最初,看一下书中给的一段加载“初始化”的代码,后果有点出其不意哦:

package org.fenixsoft.classloading;
/**
* 被动应用类字段演示一:
* 通过子类援用父类的动态字段,不会导致子类初始化 **/
public class SuperClass {
    static {System.out.println("SuperClass init!");
    }
    public static int value = 123; 
}
public class SubClass extends SuperClass {
    static {System.out.println("SubClass init!");
    } 
}
/**
* 非被动应用类字段演示 **/
public class NotInitialization {public static void main(String[] args) {System.out.println(SubClass.value);
    } 
}/* 运行后果:SuperClass init! */

​ 上面再看下如果只调用子类的静态方法会产生什么事件:

static class superClass{
    static {System.out.println("super load");
    }
}

static class SubClass extends superClass{
    static {System.out.println("sub load");
    }

    public static void test(){System.out.println("sdsad");
    }
}

public static void main(String[] args) {
 // write your code here
    SubClass.test();}/** 运行后果
    
*/

验证

​ 验证是紧接着类加载之后的步骤,验证次要的做的事件是验证以后的 class 文件是否能够被虚拟机承受,这一步是至关重要的一步,决定了虚拟机是否平安,所以虚拟机标准外面用了 N 多页的内容讲述,当然这里的验证内容也是挑重点介绍。:

​ 首先是验证文件格式,比方魔数,主次版本,常量池和索引,验证这些内容目标是避免有人篡改 class 文件构造。

​ 验证完格局紧接着是验证具体的数据,比方类是否具备父类,以及验证定义的字段和属性等是否合乎 java 的语法。这一节也叫做元数据验证,能够简略了解为对于语法等验证。

​ 之后是字节码验证,也是最简单的步骤,因为程序的运行依赖程序计数器扫描字节码指令实现,所以字节码验证次要的内容就是验证操作的“原子性”,比方 Int 操作不会变为 long 操作,同时保障栈帧的办法失常运行。

​ 最初是符号援用的验证,验证是否能通过符号援用找到类的全限定名称,验证字段是否具备可拜访性等。

​ 总的来说,验证阶段分为上面等局部:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号援用验证

筹备

​ 容易误会的一个阶段,这个阶段须要留神的事件就是筹备阶段初始化的动态变量是 final类型的动态常量。另外筹备阶段会对类变量进行初始化,然而不会呈现类的实例化,也就是说此时生成的仅仅是一个栈中的援用,能够通过援用在初始化阶段疾速构建对象然而仅仅是做了一个筹备而已,另外须要留神因为 jdk7 其实外部曾经偷偷将常量池挪动到了堆当中,所以这些变量都是在堆中生成的。

​ 上面是对于动态变量的初始化细节:

private static final int count1 = 123;
private static int count2 = 55;

​ 这里间接说后果,count 在这个阶段的值是 123,而 count2 是0。通过这个细节也能够阐明为什么很多书中倡议尽量应用 final 字段,因为它能将“初始化”的步骤提前。千里之行; 始于足下的状况下有不错的性能晋升。

解析

​ 解析的外围就是把符号援用变为间接援用,什么是符号援用,什么是间接援用呢?书中用了一大段内容形容,这里用一个案例来示意就明确了(请看上面的代码),obj 就能够被认为是一个符号援用,符号援用能够是任何没有歧义的“占位符”(当然和关键字抵触是不行的),而间接援用就是将这个占位符指向一个堆中的实例,有了间接援用也证实实例在内存中曾经开拓了空间,所以 间接援用肯定是一个指针并指向堆中一个理论地址:

public void test(){
  Object obj = null;
  obj = new Object();}

这里有个惟一的例外:invokedy namic 指令。这是 java 为了反对动静语言的个性而呈现的一个指令,除开这个指令的所有其余指令都是能够认为解析这一步骤中曾经实现了“动态化”,即指针具体指向的地址曾经确定。

​ 解析动作次要针对类或接口、字段、类办法、接口办法、办法类型、办法句柄和调用点限定符这 7 类符号援用进行。

​ 对于解析的细节,这里简要概括一下,当然这部分只须要理解四个次要步骤即可:

1. 类 / 接口解析
    不为数组,解析全限定名类加载器加载
    为数组,解析全限定名各数组元素,并生成对应数组维度的拜访对象
2. 字段解析
    本类查找解析
    接口父类查看
    父类查看
    抛出 nosuckField
3. 办法解析
    如果在 class 中发现 class_index 索引为接口,会抛出异样
    本类查找
    父类查找
    父类与接口查找
    No such method
    返回间接援用进行权限验证
4. 接口办法解析
    与办法解析相似,留神解析到 object 类为止

初始化

​ 留神这个步骤才是程序员真正认知意义上的初始化,也就是学习 java 根底的时候学到的初始化的程序,同时也是真正执行 java 代码的阶段,所以这个阶段用“分配资源”这个词可能更加适合。

​ 之前也提到过,筹备阶段会有一个类变量的构建,能够认为是.class 对象被加载到办法区,而初始化则是真正将办法区的这个援用构建到堆上。

​ 这里不得不提 <clinit >() 这个办法,此办法是在编译时候由 java 生成的,简略了解能够认为是一个类的结构器的入口,所有的类初始化必须调用这个办法,同时如果发现父类没初始化,则须要执行父类的 <clinit >(),最初如果是接口则在应用接口的常量的时候会调用<clinit >(),另外接口的实现类在初始化时也一样不会执行接口的<clinit >() 办法。

类加载的细节

​ 理解了类加载的过程,这一节来补充一些类加载的细节:

类加载根本条件

  • 加载 / 验证 / 筹备三者程序是确定的,原子化操作

Jvm 只保障程序 统一,然而不保障这三者的连贯性,意味着他们之间能够交叉其余的操作

  • 解析可能在初始化的前后

这是为了满足动静绑定的个性而设置的

  • 加载验证,筹备并不是同步实现的,会存在穿插容许的状况

程序确定,然而并不同步。

什么是被动援用?

  • 子类援用父类定义字段只会触发父类初始化
  • 数组初始化是 newarray, 并不是非法对象初始化
  • 通过 final 润饰的常量池元素

总结

​ 本文次要围绕了类加载的过程这一个要点进行了温习,能够看到类加载的过程还是绝对比拟好了解的,须要特地关注的内容一个是筹备阶段和初始化阶段,这里也有可能是一个踩坑点。

写在最初

​ 下一篇文章将会持续深刻类加载器和双亲委派机制,当然在系列很早的文章也有提到过,下一节将是对于类加载器内容的深刻和扩大,以及 jdk9 模块化对于类加载器的影响。

退出移动版