JVM系统学习(03):Java虚拟机是如何加载类的

image.png

前言


从class文件到内存中的类,需要经过加载、链接以及初始化三个大的步骤。其中,链接过程需要验证;而内存中的类没有经过初始化也同样是不能使用的。那么,又是否所有的Java类都需要经过这三步呢?这也是这篇文章想要弄清楚的。

我们知道Java语言的类型可以分为两大类:基本数据类型和引用数据类型。在上一篇文章JVM系统学习(02):Java的基本类型中,有详细介绍8大基本数据类型及特性,知道了他们是有JVM提前预定义好的。

至于另一大引用数据类型,Java又将其细分为4中:类、接口、数组类和泛型参数。因为泛型参数在编译过程中会被擦除(关于泛型擦除的概念后再后面笔记中专门记录),所以在JVM中实际上只有前3种引用类型。在类、接口和数组类中,数组类是由JVM直接生产的,而类和接口则有对应的字节流。

提到字节流,最常见的便是由Java编译器生成的class文件。处理class文件之外,我们也可以在程序内部直接生产,或者从网络中获取(比如我们都听过的嵌入在网页中的java applet,但是上基本很少甚至压根没这么用过的)字节流。这些不同形式的字节流都会被加载到JVM中,形成类或接口。为了便于描述,我把接口和类用“类”来统称。

无论是直接生成的数组类,还是加载的类,JVM都需要对其进行链接和初始化。下面来看下每一个步骤的细节。

加载


加载,是指查找字节流,并且创建类的过程。前言中有提到数组类没有对应的字节流,而是由JVM直接生成的。对于其他类来说,JVM需要借助类加载器来完成查找字节流的过程。

以村里盖房子为例,村里刘备家需要盖房子,那么按照流程刘备需要先找个建筑设计师,跟他说想要设计一个房型(比如:“3房、2厅、2卫、1厨”)。其实这里的房型就相当于类,而设计师相当于类加载器。可能还不是很理解,继续往下走。

村里有很多的建筑师,他们等级制度森严,但都有共同的祖师爷,叫启动类加载器(bootstrap class loader)。启动类加载器是由C语言实现的,没有对应的Java对象。换句话说祖师爷不喜欢刘备这样的小角色去打扰他,所以村里谁也没有祖师爷的联系方式。

除了启动类加载器之外,其他的类加载器都是java.lang.ClassLoader的子类,所以其他的类加载器都有对应的Java对象。这些类加载器需要先有另外一个类加载器(比如说启动类加载器)加载至JVM中,方能执行类。

村里的建筑师有一个潜规则,就是接到单子,自己不能直接开始干,得先给师傅过目。师傅不接手的情况下,才能自己来。在JVM中这个潜规则有一个特别的名字,叫双亲委派模型。每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,在由自己尝试去加载。

在Java9之前,启动类加载器负责加载最基础、最为重要的类,比如存在在Jre的lib目前下jar包中的类以及有JVM参数-Xbootclasspath指定的类。处理启动类加载器之外,另外两个重要类加载器是扩展加载器(extension class loader)应用类加载器(application class loader),均由Java的核心类库提供。

扩展类加载器的父类是启动类加载器。它复杂加载相对次要、且又通用的类,比如存放在JRE/lib/ext目录下jar包中的类以及由系统变量java.ext.dirs指定的类。

应用类加载器的父类是扩展类加载器。它复杂加载应用程序路径下的类(这里的应用程序路径是指虚拟机参数 -cp/-classpath、系统变量java.class.path或环境变量CLASSPATH所指定的路径)。默认情况下,应用程序中包含的类便是由应用类加载器负责加载的。

Java9引入了模块系统,并微弱的更改了上述的类加载器(官方文档:https://docs.oracle.com/javase/9/migrate/toc.htm#JSMIG-GUID-A868D0B9-026F-4D46-B979-901834343F9E)。扩展类加载器被该名称平台类加载器(platform class loader)。Java SE中除了几个少数的关键模块,比如说java.base是由启动类加载器加载之外,其他的模块均有平台类加载器所加载。

除了又java的核心类库提供的类加载器之外,我们还可以加入自定义类加载器来实现特殊的加载方式。举例来说我们可以对class文件加密,加载时再利用对应的类加载器对其进行解密。

处理加载功能之外,类的唯一性是由类加载器示例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

链接


链接,是指将创建成的类合并到JVM中,使之能够执行的过程。它可以分为验证、准备以及解析三个阶段。

验证阶段的目的,确保被加载的类满足JVM的约束条件。

准备阶段的目的,为被加载类的静态字段分配内存。Java代码中对静态字段的具体初始化,则会在下面的初始化阶段进行。

处理分配内存外,部分Java虚拟机还会在此阶段构造跟类层次先关的数据结构。比如说用来实现虚方法的动态绑定的方法表。

在class文件被加载至JVM之前,这个类无法知道其他类及其方法、方法所对应的的具体地址,甚至不知道自己方法、字段的地址。因此每当需要引入这些成员时,JVM会生成一个符号引用。在运行阶段,这个符号引用能够无歧义的定位到具体的目标上。

举例来说,例如一个方法调用,编译器会生成一个包含目标方法所在类的名称、目标方法的名称、方法入参类型以及返回值类型的符号引用,来指代所要调用的方法。

解析阶段的目的,正是要将这些符号引用解析成实际引用。如果符号引用所指向一个未被加载的类、方法或者字段,那么解析将会触发这个加载。(触发加载,但未必会触发链接和初始化)。

JVM规范并未要求在连接过程中完成解析。它近规范了:如果一写字节码引用了一些符号引用,那么在字节码执行之前必须解析到真实引用。

初始化


在Java代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码中对其赋值。

如果直接赋值的对象被final修饰,并且他的类型是基本类型或者字符串时,那么该字段便会被Java编译器标记为常量值,其初始化直接由Java虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被Java编译器至于同一个方法中,并把它命名为<clinit>.

类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行<clinit>方法的过程。Java虚拟机会通过加锁来保证一个<clinit>只会被执一次。

那么,类的初始化何时被触发呢?JVM规范枚举了下面几种情况:
1.当虚拟机启动时,初始化用户指定的主类。
2.当遇到用于信息目标类实例的new指令时,初始化new指令的目标类
3.当遇到调用静态方法的指令时,初始化静态方法所在的类
4.当遇到访问静态字段的时候,初始化改静态字段所在的类
5.子类的初始化会触发父类的初始化
6.如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化。
7.使用反射API对某个类进行反射调动时,初始化这个类
8.当初次调用MethodHandle实例时,初始化改MethodHandle所指向方法所在的类

public class Singleton {

  private Singleton() {}

  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
  }

  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

这段代码是著名的单例延迟初始化例子,只有当调用Singleton.getInstance()方法时,程序才会访问LazyHolder.INSTANCE的静态属性,才会去触发LazyHolder的初始化,继而新建一个singleton实例。

总结


这篇笔记中记录了一个class文件的字节流 通过 JVM 转换为 Java类的过程。这个过程分为加载、链接以及初始化3个过程。

加载指查找字节流,并据此创建类的过程。加载需要借助类加载器,在JVM中类加载器使用了双亲委派模型,即接收到改请求是,会将请求先转发给当前类加载器的父类加载器取查找。

链接是指将创建成的类合并到JVM中,使其能够执行的过程。链接还分为验证、准备和解析三个阶段。其中,解析阶段是非必要的。

初始化是为标记为常量的字段进行赋值,以及执行<cliinit>的过程。类的初始化仅被执行一次,这个特征可被用来实现单例的延迟加载。

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

推荐阅读更多精彩内容