虚拟机类加载机制

前言

Class文件结构已经学习完毕,今天来学习下虚拟机如何加载Class文件。

C语音编译连接后直接就生成了可执行文件,程序执行,并不需要额外操作。但Java不一样,类型的加载和连接都是在程序运行期间完成的。

这会导致额外的开销,但这也给Java带来了无比的灵活性,比如动态加载技术正是基于此特性完成的。

本文主要主要内容如下:

  • 类加载时机
  • 类加载过程
  • 类加载器

类加载时机

类从被加载到虚拟机内存开始,到卸载出内存为止,一共会经历 加载、验证、准备、解析、初始化、使用、卸载 7个阶段。其中 验证、准备、解析 这三个解析被称为连接过程。

一般来说,以上过程会按部就班地开始,但只是开始,因为这些阶段会交叉进行,通常会在一个阶段开始后激活调用另一个过程。

以下4种情况下,一定会开始类的加载过程:

  • 遇到new、getstatic、putstatic、invokestatic等字节码指令时,而类没有经过初始化。生成这4个指令的常见情况:使用new关键字实例化对象、读取或设置类的静态字段(使用final关键字修饰除外)、调用类的静态方法

  • 使用java.lang.reflect进行反射调用,而类没有经过初始化。

  • 当初始化一个类时,发现其父类还没有被初始化,则需要先初始化其父类

  • 当虚拟机启动时,用户需要指定一个主类(包含main方法),虚拟机会初始化这个主类。

回顾第1条,为啥final关键字修饰的静态成员变量会例外呢?回顾Class文件结构常量池知识,如果是final关键字修饰的static变量,它的值使用ConstantValue进行初始化,非final修饰的static变量在 clinit 方法中初始化。

以上4种场景被虚拟机称为有且只有的会触发类初始化的场景,这4种场景被称为对类的主动引用。除此之外的所有场景都不会触发类的初始化,被称为被动引用。

public class SuperClass {
    /*
     * 被动引用父类静态变量,不会初始化子类
     */
    static{
        System.out.println("super class init");
    }
    public static int value = 123;
}

public class SubClass extends SuperClass{
  static{
      System.out.println("sub class init");
  }
}

public class ConstClass {
  static{
      System.out.println("const class init");
  }
  public static final String HELLO = "hello world";
}

/*
 * 被动引用父类静态变量,不会初始化子类
 */
public static void invokeSuperStatic(){
    System.out.println(SubClass.value);
}

/*
 * 通过数组定义来引用类,不会触发类的初始化
 */
public static void accessByArray(){
    SuperClass[] array = new SuperClass[10];
}

/*
 * 访问final static变量,不会初始化类
 */
public static void accessFinalField(){
    System.out.println(ConstClass.HELLO);
}

如上代码,一共对应了3种被动引用方式。

  • 引用父类的静态变量,不会初始化子类
  • 通过数组定义来引用类,不会触发类的初始化。此时没有初始化 SuperClass 类,但初始化了 [Lcom.okunu.jvm.init 这个类,它由字节码指令newarray触发,且实现了数组的 length 等方法。数组也是对象,是Java自动生成的对象,所以它并不会去初始化数组中的元素类。
  • static final类型的常量,在编译阶段已把此常量存储到了NotInit类的常量池了,对常量 HELLO 的引用已经转化对自身常量池的引用了,所以不会初始化ConstClass类了

类加载过程

类加载过程分为 加载、验证、准备、解析、初始化、使用、卸载 7个阶段,本文中主讲前面5个阶段

1、加载

加载阶段主要做以下事情:

  • 通过类的全限定名获取类的二进制文件流
  • 将字节流所代表的静态存储结构转变为方法区的运行时数据结构
  • 在堆中生成此类的 java.lang.Class 对象,作为方法区这些数据的访问入口

加载阶段是开发可控性最强的阶段,比如说开发可以使用自定义类加载器去加载某个类,类的来源也可以是jar包、class文件、甚至是网络流。

2、验证

验证是连接过程的第1步,它是为了确保Class文件的字节流符合虚拟机的规范。

加载阶段加载Class文件,并未规定Class文件的来源,如果愿意,甚至可以手动编写Class文件,但这样的Class文件有可能不符合虚拟机规范,所以需要验证。它主要进行如下方面验证:

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

3、准备

准备阶段是为类变量正式分配内存并赋默认值的阶段,这些内存都在方法区内分配。

有两个点需要强调:

  • 只为类变量分配内存,即是为static变量分配内存,一般成员变量是在类被实例化时分配内存
  • 只会为static变量赋默认值

假设有如下static变量

  public static int value = 123

那么准备阶段,value的值会变成0,而不是123。而把value赋值为123的putstatic指令是在程序被编译后,存放在 clinit 方法中,所以value被赋值为123 发生在 初始化 阶段,即第5个阶段。

如果是final修饰的static变量呢?如果字段的字段属性表中存在ConstantValue属性,那么准备阶段,变量就会被初始化为ConstantValue存储的值。

假设上述变量为

  public static final int value = 123

编译时javac会为value生成ConstantValue属性,准备阶段value的值就会成为123。

4、解析

解析阶段是将常量池中的符号引用替换为直接引用的过程。

符号引用是指字面量,能无歧义地定位到目标就好,它与虚拟机的内存布局无关。

直接引用,是指可以直接访问到对象的指针或句柄,与内存布局相关的。

解析主要针对类或接口、字段、类方法、接口方法4类符号引用。

5、初始化

初始化是类加载过程的最后一步,除了加载过程,开发可以使用自定义类加载器参与外,其它过程全都是虚拟机自己控制着。初始化阶段,才真正开始执行Java程序代码。

准备阶段,为static变量分配内存并赋默认值,而初始化阶段则会执行clinit方法,为static变量分配代码中所指定的值。

  • <clinit>方法是虚拟机自动生成的,编译器收集类中类变量的赋值语句、static静态语句块合并产生<clinit>方法。收集的顺序是代码顺序

  • <clinit>方法和 init 方法不同,它不需要显示调用父类<clinit>方法,因为虚拟机保证在调用子类<clinit>方法前,父类<clinit>方法已经执行完毕。

  • 因为父类的<clinit>方法先执行,所以父类静态语句块要优先于子类的静态语句块。

  • <clinit>方法并不是必须的,如果代码中没有定义静态变量或者静态语句块,则没有<clinit>方法。

类加载器

类加载器的作用就是实现加载阶段的任务,通过一个类的全限定名获取Class文件的二进制字节流。

如果同一个类由两个不同的类加载器加载,得到两个实例,那么这两个实例在虚拟机看来,并不同一种类型。

类加载器可以分成三类:

  • 启动类加载器(Bootstrap ClassLoader),负责将存放在 JAVA_HOME\lib 目录下并且是虚拟机识别的类库加载到虚拟机内存当中来

  • 扩展类加载器(Extension ClassLoader),它负责加载存放在 JAVA_HOME\lib\ext 目录下的类库

  • 用户程序类加载器(Application ClassLoader),它是默认的类加载器,负责加载ClassPath上指定的类库,如果某个类没有特殊指定类加载器,就使用它。

我们的应用程序都是由这三类类加载器完成类加载的,如果需要,我们还可以实现自定义的类加载器完成类加载。它们的关系如下图所示:

上图所展示的类加载器关系,就叫类加载器的双亲委派模型。除了顶层的启动类加载器,其它的类加载器都有自己的父类加载器,但它们不是通过继承实现的,而是通过组合实现的。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,会先请求自己的父类加载器去加载,如果父类无法加载该类,子类才会尝试自己去加载。

protected Class<?> loadClass(String name, boolean resolve){
    Class c = findLoadedClass(name);
    if (c == null) {
        if (parent != null) {
            //使用父加载器加载此类
            c = parent.loadClass(name, false);
        } 
        if (c == null) {
            // 如果父加载器没有成功加载,则自己尝试加载
            c = findClass(name);
        }
    }
    return c;
}

查看ClassLoader类的loadClass方法,双亲委派模型从上述代码中实现。

使用双亲委派模型,Java类和它的类加载器一起具备了一种带有优先级的层次关系。例如Object类,无论哪个类加载器来加载它,最后都要委托启动类加载器来加载Object,这就保证了Object在程序的各个类加载器环境中都是同一个类。Java的基本类,基础行为必须被保证,不能被篡改,这就是双亲委派模型的意义所在。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容