深入理解JVM虚拟机读书笔记——类的加载机制-阿里云开发者社区

开发者社区> 兴趣使然的草帽路飞> 正文

深入理解JVM虚拟机读书笔记——类的加载机制

简介: 注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复 001 获取。
+关注继续查看

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。


1. 类加载的时机

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如下图(类的生命周期)所示:



image.png

2. 类加载的过程

接下来我们会详细了解Java虚拟机中类加载的全过程,即加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。


2.1 加载

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:


1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

2.2 验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。


验证阶段可以分为如下几个步骤:


1.文件格式验证:


第一阶段验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:


是否以魔数0xCAFEBABE开头。

主、次版本号是否在当前Java虚拟机接受范围之内。

常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。

指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。

Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

2.元数据验证:


第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,这个阶段可能包括的验证点如下:


这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。

这个类的父类是否继承了不允许被继承的类(被final修饰的类)。

如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。

类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。

3.字节码验证:


第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。


在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:


保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。

保证任何跳转指令都不会跳转到方法体以外的字节码指令上。

保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

4.符号引用验证:


最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用[3]的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。本阶段通常需要校验下列内容:


符号引用中通过字符串描述的全限定名是否能找到对应的类。

在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。

符号引用中的类、字段、方法的可访问性(private、protected、public、< package>)是否可被当前类访问。

2.3 准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段(是这里所说的初始值“通常情况”下是数据类型的零值)。


假设一个类变量的定义为:

public static int value = 123;

那变量value在准备阶段过后的初始值为0而不是123。

基本数据类型的零值表:

image.png

2.4 解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。


符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

解析的具体流程分为如下几个阶段:


1.类或接口的解析

2.字段解析

3.方法解析

4.接口方法解析

2.5 初始化

进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序代码去初始化类变量和其他资源(例如,静态变量赋值动作和静态语句块(static{})中的语句)。


我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器< clinit>()方法的过程。


3. 类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。


这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。


站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[1],是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。


类加载器分类,以 JDK 8 为例:


image.png

类加载器的优先级(由高到低):启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器

3.1 启动类加载器

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load;

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}

执行:

package cn.itcast.jvm.t3.load;

public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        // aClass.getClassLoader():获得aClass对应的类加载器
        System.out.println(aClass.getClassLoader());
    }
}

输出:

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
bootstrap F init
null

-Xbootclasspath 表示设置 bootclasspath

其中 /a:. 表示将当前目录追加至 bootclasspath 之后

可以有以下几个方式替换启动类路径下的核心类:

java -Xbootclasspath: < new bootclasspath>

前追加:java -Xbootclasspath/a:<追加路径>

后追加:java -Xbootclasspath/p:<追加路径>


image.png

3.2 扩展类加载器

package cn.itcast.jvm.t3.load;

public class G {
    static {
        System.out.println("classpath G init");
    }
}

程序执行:

public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

输出结果:

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 // 这个类是由应用程序加载器加载

写一个同名的类:

package cn.itcast.jvm.t3.load;

public class G {
    static {
        System.out.println("ext G init");
    }
}

打个 jar 包:

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class // 将G.class打jar包
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到JAVA_HOME/jre/lib/ext扩展类加载器加载的类必须是以jar包方式存在),重新执行 Load5_2

输出:

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44 // 这个类是由扩展类加载器加载

3.3 应用程序加载器

应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader() 方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。


3.4 自定义类加载器

什么时候需要自定义类加载器:


1)想加载非 classpath 随意路径中的类文件

2)都是通过接口来使用实现,希望解耦时,常用在框架设计

3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:


继承 ClassLoader 父类。

要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制。

读取类文件的字节码。

调用父类的 defineClass 方法来加载类。

使用者调用该类加载器的 loadClass 方法。

4. 双亲委派模型

什么是双亲委派模型?


如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。


如下图所示:



image.png

为什么要使用双亲委派模型呢?(好处)


避免重复加载 + 避免核心类篡改:


采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父加载器已经加载了该类时,就没有必要子加载器再加载一次。

其次是考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心API库被随意篡改。

结语:

非常建议学习Java的小伙伴,买一本周志明老师的《深入理解Java虚拟机(第3版)》去读一读,博客和视频教程,始终不如看书来得实在呀!


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
8780 0
虚拟机类加载机制的了解
本文对java虚拟机的类加载机制进行了研究,该过程主要分为三个步骤:加载、连接、初始化,其中连接步骤可以分为:验证、准备、解析三个步骤。文中对于以上步骤进行了详细的介绍。此外,本文还涉及类加载器的部分知识:类加载器可以分为:启动类加载器、扩展类加载器、系统类加载器:java虚拟使用双亲委派的方式加载类。
1037 0
深入理解JVM虚拟机读书笔记——类的加载机制
注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复 001 获取。
6 0
深入理解计算机系统结构——虚拟存储器
虚拟存储器 虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟存储器提供了三个重要的能力: (1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
989 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10597 0
《深入解析Android 虚拟机》——2.5 内存异常和垃圾处理
对于C和C++的开发人员来说,在内存管理领域应该能够游刃有余。在计算机系统中,内存负责维护每一个对象生命的从开始到终结。Java内存分配与管理是Java的核心技术之一,通常Java在内存分配时会涉及到以下区域。
1784 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
11959 0
19、深入理解计算机系统笔记,虚拟存储器,基本原理(1)
1、一个系统中的进程是与其他进程共享CPU和主存资源的。 2、虚拟存储器(Virtual Memory),是硬件异常,硬件地址翻译,主存,磁盘文件和内核软件的完善交互,它为每个进程提供一个大的,一致的,私有地址空间。
718 0
20、深入理解计算机系统笔记,虚拟存储器,基本原理(2)
2)页命中 3)缺页 DRAM缓存不命中称为缺页(page fault)。 VM page fault (before). The reference to a word in VP 3 is a miss and triggers a page fault.
542 0
+关注
兴趣使然的草帽路飞
我是路飞,一个双非本科出身的Java程序员,22届校招收获阿里、美团、哔哩哔哩意向书,目前是阿里大家庭中的一名全栈开发工程师。我的博客地址(CSDN 10万粉丝博主、博客专家):csp1999.blog.csdn.net
87
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载