Java魔法堂:类加载机制入了个门
一、前言
当在CMD/SHELL中输入 $ java Main<CR><LF> 后,Main程序就开始运行了,但在运行之前总得先把Main.class及其所依赖的类加载到JVM中吧!本篇将记录这些日子对类加载机制的学习心得,以便日后查阅。若有纰漏请大家指正,谢谢!
以下内容均基于JDK7和HotSpot VM。
二、执行java的那刻
大家都知道通过java命令来启动JVM和运行应用程序,但实际的流程又是如何的呢?
1. 首先根据java后的运行模式配置项或<JAVA_HOME>/jre/lib/i386/jvm.cfg来决定是以client还是server模式运行JVM,然后加载<JAVA_HOME>/jre/bin/client或server/jvm.dll,并开始启动JVM;
2. 在启动JVM的同时将加载Bootstrap ClassLoader(启动类加载器,使用C/C++编写,属于JVM的一部分);
3. 通过Bootstrap ClassLoader加载sun.misc.Launcher类(ExtClassLoader和AppClassLoader是它的内部类);
4. sun.misc.Launcher类在执行初始化阶段时,会创建一个自己的实例,在创建过程中会创建一个ExtClassLoader(扩展类加载器)实例、一个AppClassLoader(系统类加载器)实例,并将AppClassLoader实例设置为主线程的ThreadContextClassLoader(线程上下文类加载器)。
5. 然后AppClassLoader实例就开始加载Main.class及其所依赖的类库了。
二、类加载的流程
1. 加载(Loading)
2. 链接(Linking),细分为:验证(Verification)、准备(Preparation)和解析(Resolution)
3. 初始化(Initialization)
4. 使用(Using)
5. 卸载(Unloading)
注意:加载、链接、初始化三个阶段是交叉混合进行的,并不是加载完成后才执行链接,也不是链接完成后才执行初始化的。
通过 -XX:+TraceClassLoading 可查看类加载的信息。
三、加载阶段
在整个类加载机制中,仅加载阶段可被程序员控制,其余阶段均由JVM完全掌控。
共分为3个步骤:
1. 通过类加载器根据一个类的二进制名称(Binary Name)获取定义此类的二进制字节流,在读取类的二进制字节流时链接阶段的验证操作的文件格式验证已经开始,只有通过了文件格式验证后才能存储到方法区,若验证失败则抛出 java.lang.VerifyError 或其子异常类。(文件格式验证用于保证读取的数据能够正确解析并存储在JVM堆栈中的方法区。Class文件格式由JVM规范规定,而方法区的数据结构则有各JVM自行决定)
二进制字节流的来源低是多样的,下面列举一部分:
a. 将二进制名称(如com.fsjohnhuang.test.Main)转换为平台相关的文件系统路径(linux下为com/fsjohnhuang/test/Main.class),然后相对与类加载器查找对应的类文件;
b. 按a的做法将二进制名称转换为文件系统路径,然后类加载器管辖范围下的JAR、EAR和WAR等归档文件中查找类文件;
c. 通过网络获取二进制字节流。
2. 将字节流所代表的静态存储结构(Class文件结构)转化为方法区的运行时数据结构。
3. 在内存中生成一个代表类或接口的 java.lang.Class 实例,作为操作该类或接口元数据的入口(Reflection就是利用Class实例的)。
注意:
1. 对于short boolean char int float long double基本数据类型是无需执行类加载的;
2. 对于数据类型的加载,实质上加载的是数组的组件类型(String[]数组的组件类型为String),然后由JVM内部生成一个[Ljava.lang.String的数组类型(在字节码中标识为[Ljava/lang/String;)。因此Java中操作数组时不会像C/C++那样出现数组越界的问题。
四、链接阶段
链接阶段又细分为 验证(Verification)、准备(Preparation)和解析(Resolution) 3个子阶段
解析(Resolution)不一定在类加载时执行,有可能在运行时才执行。
验证(Verification)
验证文件格式验证、 元数据验证、 字节码验证 和 符号引用验证 4个操作。
1. 文件格式验证
首先对于被反复使用和验证过的类,验证过程是非必要的。可以通过 -Xverify:none 来关闭验证,可缩短虚拟机加载的时间。
操作对象:二进制字节流
目的:验证是否符合Class文件格式的规范。
2. 元数据验证
操作对象:方法区中的类或接口的信息
目的:对字节码描述的类的元数据信息进行语义分析,保证符合Java语言规范。
类的元数据信息包括:
a. 父类信息(全限定名、修饰符等);
b. 父类字段、方法信息;
c. 类的信息(全限定名、修饰符等);
d. 类的字段、方法信息;
等等。注意:不含方法体信息!
3. 字节码验证
操作对象:方法区中的类信息的Code属性
目的:对方法体语句进行语义分析,保证方法运行时不会出现危害JVM安全的事件
由于这种语义分析需要执行类似于下列等检查,因此需要进行类型推导这一十分耗时的操作。
1. 检查操作数栈的数据类型与指令的操作数类型是否兼容;
2. 检查跳转指令不会跳转到方法体外的字节码指令上;
3. 检查类型转换是安全的。
JDK1.6在Code属性中添加了一个StackMapTable的属性,用于描述方法中所有基本块(Basic Block,按控制流拆分的代码块)开始时本地变量表和操作数栈引用的状态。然后字节码验证时则进行类型检查而不是类型推导,从而提高验证的性能。可通过 -XX:-UseSplitVerifier 来关闭类型检查回归到类型推导,或通过 -XX:+FailOverToOldVerifier 来设置当类型检查失败就采用类型推导。
JDK1.7则只能采用类型检查了。
但StackMapTable的数据依然可以被篡改,而这就是JVM开发团队需要考虑的了。
注意:字节码验证时会触发父类或所实现的接口的符号引用的解析(也就是会触发类加载过程)。
4. 符号引用验证
操作对象:方法区中的类或接口信息
目的:对类的符号引用和类的实际信息(类、字段、方法)进行验证,保证符号引用可成功解析为直接引用,并当前类可以成功访问直接引用
在执行链接阶段的解析子阶段时,会对符号引用进行符号引用验证,验证包括以下等内容:
a. 通过符号引用中字符串描述的全限定名是否可以在方法区中找到对应的类。
b. 通过符号引用中对字段、方法的简单名和描述符是否可以在方法区找到对应的字段和方法。
c. 当前实例是否有权限访问符号引用的类、字段和方法。
若验证失败则会抛出 java.lang.IncompatibleClassChangeError 的子类 java.lang.IllegalAccessError 、 java.lang.NoSuchFieldError 和 java.lang.NoSuchMethodError 等。
准备(Preparation)
在方法区为类变量分配内存空间,并初始化为0。示例如下:
// 经过准备阶段后,value类变量将存储在方法区中,值为0。123的赋值操作将在初始化阶段进行。
public static int value = ; // 对于类常量(类静态常量),则直接初始化为ConstantValue属性的值。
// 经过准备阶段后,value类变量将存储在方法区中,值为123。
public static final int value = ;
各类型的零值
int
long 0L
short (short)
char '\u0000'
byte (byte)
boolean false
float 0.0f
double 0.0d
reference null
解析(Resolution)
再次强调不一定要在类加载时执行,可以在运行时调用时才执行准备阶段。
准备阶段实质就是将常量池内的符号引用替换为直接引用。
符号引用(Symbolic References):以一组符号来描述所引用的目标(类、接口、方法、字段等)。只要能无歧义地定位到目标即可,并且与JVM的实际内部布局无关,而引用的目标也不一定已经加载到内存中。符号引用的形式已经由JVM规范规定了。
直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。如果有了直接引用则目标必定已经在内存中存在了。
在执行newarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfiled和putstatic这16个字节码指令执行前先对它们使用的符号引用进行解析。
除了invokedynamic指令外,其他指令触发符号引用解析为直接引用后,将会对直接引用作缓存避免重复解析。(或者不作缓存,但JVM会保证第一解析成功则后续也会解析成功,失败则后续解析一样会收到相同的异常)。而invokedynamic则每次解析均不同。
解析主要针对类或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、类方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_info)、方法类型(CONSTANT_MethodType_info)、方法句柄(CONSTANT_MethodHandle_info)和调用点限定符(CONSTANT_InvokeDynamic_info)7种符号引用进行。(后三种是JDK1.7新增的动态语言支持信息相关)
1. 类或接口的解析
将类D中的符号引用N解析为直接引用C,首先将N的全限定名传递给D的类加载器去加载类C,然后进过加载、验证、准备阶段,并因为字节码验证而加载父类或实现的接口。一旦任何一个类或接口的加载失败则符号引用N解析为直接应用C的操作就会被宣告失败
成功解析后则进行符号引用验证,检查D是否具备访问C的权限。若不具备则抛出`java.lang.IllegalAccessError`。
2. 字段的解析
首先对`CONSTANT_Fieldref_info`的`class_index`项所指向的符号引用进行类或接口解析。若解析成功后得到类或接口的直接引用C,则在C中查找简单名称和字段描述符与`CONSTANT_Fieldref_info`的`name_index`项所指向的内容相匹配的直接引用,若失败则从下往上递归搜索C所实现的接口中是否有匹配的,若失败则从下往上递归搜索C所实现的父类中是否有匹配的,若失败则抛出`java.lang.NoSuchFieldError`。
若成功解析直接引用,则进行符号引用验证,失败则抛出`java.lang.IllegalAccessError`。
3. 类方法的解析
首先对`CONSTANT_Methodref_info`的`class_index`项所指向的符号引用进行类或接口解析。若解析成功后得到类或接口的直接引用C,则在C中查找简单名称和字段描述符与`CONSTANT_Methodref_info`的`name_index`项所指向的内容相匹配的直接引用,若失败则从下往上递归搜索C所实现的父类中是否有匹配的,若失败则从下往上递归搜索C所实现的接口中是否有匹配的(若成功说明C是一个抽象类并抛出`java.lang.AbstractMethodError`),若失败则抛出`java.lang.NoSuchMethodError`。
若成功解析直接引用,则进行符号引用验证,失败则抛出`java.lang.IllegalAccessError`。
4. 接口方法的解析
首先对`CONSTANT_InterfaceMethodref_info`的`class_index`项所指向的符号引用进行接口解析。若解析成功后得到类或接口的直接引用C(若C不是接口则抛出`java.lang.IncompatibleClassChangeError`),则在C中查找简单名称和字段描述符与`CONSTANT_InterfaceMethodref_info`的`name_index`项所指向的内容相匹配的直接引用,若失败则从下往上递归搜索C的父接口中是否有匹配的,若失败则抛出`java.lang.NoSuchMethodError`。
五、初始化阶段
类和接口均有初始化过程,实质上就是执行字节码中的`<clinit>`构造函数。
类中静态字段和静态代码块均被代码重排到`<clinit>`函数中进行赋值等操作。并且父类必须已经初始化后再初始化子类。
接口的静态字段也被代码重排到`<clinit>`函数中进行赋值操作。但不要初始化该接口前必须其父接口完成了初始化,而是在真正使用到父接口(静态常量字段)时才触发初始化。
JVM会自动处理多线程环境下`<clinit>`函数的同步互斥执行。因此若在`<clinit>`执行耗时的操作则会阻塞其他线程的执行。
主动引用
JVM规范规定以下5种情况,则必须执行初始化(加载、链接自然会在之前进入执行状态)
1. 遇到new, getstatic, putstatic或invokestatic这4条字节码指令时,若类没有进行过初始化,则需要先触发初始化。对应的Java代码为通过关键字new一个实例,读或写一个类变量,调用类方法。
2. 使用`java.lang.reflect`包中的方法操作类时,若类没有进行过初始化,则需要先触发初始化。
3. 当初始化一个类时,若其父类还没初始化则会先初始化父类。
4. 当虚拟机启动时,虚拟机会初始化入口函数所在的类。
5. JDK1.7增加动态语言的支持。如果一个`java.lang.invoke.MethodHandle`实例最后的解析结果是REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,而这个句柄所在的类没有进行初始化,则需要先触发初始化。
除了上述5种情况外,其他引用类的方式是不会触发初始化的,并称为被动引用。下列示例则为被动引用
1. 通过子类访问父类静态字段不会导致子类初始化,仅仅会导致父类初始化。
2. Java代码中创建数组对象,不会导致数组的组件类(如SuperClass[]的组件类为SuperClass)初始化。因为创建数组类的字节码指令是newarray。
3. 类A访问类B的静态常量不会导致类B的初始化。因为在编译阶段会将类使用到的常量直接存储到自身常量池的引用中,因此实际上运行时类A访问的是自身的常量与类B无关系。
六、总结
若有纰漏请大家指正,谢谢!
尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/4283511.html ^_^肥仔John
七、参考
《深入理解Java虚拟机 JVM高级特性与最佳实践》
Java魔法堂:类加载机制入了个门的更多相关文章
- Node魔法堂:NPM入了个门
一.前言 NPM作为Node的模块管理和发布工具,作用与Ruby的gem.Python的pypl或setuptools.PHP的pear和.Net的Nuget一样.在当前前端工程化极速狂奔的年代,即使 ...
- Java魔法堂:类加载器入了个门
一.前言 <Java魔法堂:类加载机制入了个门>中提及整个类加载流程中只有加载阶段作为码农的我们可以入手干预,其余均由JVM处理.本文将记录加载阶段的核心组件——类加载器的相关信息,以便日 ...
- 【转】Java魔法堂:String.format详解
Java魔法堂:String.format详解 目录 一.前言 二.重载方法 三.占位符 四.对字符.字符串进行格式化 五.对整数进行格式化 六. ...
- java 虚拟机的类加载机制
Java 虚拟机的类加载机制 关于类加载机制: 虚拟机把描述类的数据从Class 文件加载到内存,并对数据进行效验.转换解析和初始化,最终 形成可以被虚拟机直接使用的Java 类型,就是虚拟机的类 ...
- Java基础:类加载机制
之前的<java基础:内存模型>当中,我们大体了解了在java当中,不同类型的信息,都存放于java当中哪个部位当中,那么有了对于堆.栈.方法区.的基本理解以后,今天我们来好好剖析一下,j ...
- Java魔法堂:打包知识点之jar
一.前言 通过eclipse导出jar包十分方便快捷,但作为码农岂能满足GUI的便捷呢?所以一起来CLI吧! 二.JAR包 JAR包是基于ZIP文件格式,用于将多个.java文件和各种资源文件, ...
- 深入理解Java虚拟机(八)——类加载机制
是什么是类加载机制 Java虚拟机将class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是类加载机制. 类的生命周期 一个类从加载到内存 ...
- 【进阶之路】深入理解Java虚拟机的类加载机制(长文)
我们在参加面试的时候,经常被问到一些关于类加载机制的问题,也都会在面试之前准备的时候背好答案,但是我们是否有去深入了解什么是类加载机制呢?这段时间因为一些事情在家看了些书,这次就和大家分享一些关于Ja ...
- Java虚拟机之类加载机制
⑴背景 Java虚拟机把Class文件加载到内存中,并对数据进行校验,转换解析,和初始化,最终形成被虚拟机直接使用的Java类型,这就是类加载机制. ⑵Jvm加载Class文件机制原理 类的生命周 ...
随机推荐
- 双击防止网页放大缩小HTML5
幕双击放大或缩小.即相当于这样设置 <meta name="viewport" content="width=device-width, initial-scale ...
- CSS:谈谈栅格布局
检验前端的一个基本功就是考查他的布局.很久之前圣杯布局风靡一时,这里就由圣杯布局开始,到最流行的bootstrap栅格布局. 圣杯布局 圣杯布局是一种三列布局,两边定宽,中间自适应: * { box- ...
- Do带你解析:原生APP与web APP的区别
对于DeviceOne原生跨平台APP与WEB APP的区别,很多人还不是很清楚,下面就让小编来简单介绍DeviceOne原生APP的功能以及与WEB APP的区别. 定义,什么是原生APP和web ...
- 作业二:Github注册过程
第一步.打开Github官网https://github.com/ ,在相应位置填写注册名.注册邮箱.注册密码完成后点击注册. 第二步.这时会弹出一个界面,让你选择你的私人计划(personal pl ...
- Entity Framework 5.0系列之EF概览
概述 在开发面向数据的软件时我们常常为了解决业务问题实体.关系和逻辑构建模型而费尽心机,ORM的产生为我们提供了一种优雅的解决方案.ADO.NET Entity Framework是.NET开发中一种 ...
- 解决ng界面长表达式(ui-set)
本文来自网友sun shine的问题,问题如下: 您好, 我想求教一个问题. 在$scope中我的对象名字写的特别深, 在 html中我又多次用到了同一个对象, 对不对在 html中让它绑定到一个临时 ...
- TCP Server—Linux
#include <stdio.h> #include <netinet/ip.h> #define BUFF_SIZE 1024 int main(int argc,char ...
- [专业名词·硬件] 1、等效串联电阻ESR概述及稳压电路中带有一定量ESR电容的好处
一.等效串联电阻ESR概述 ESR是Equivalent Series Resistance的缩写,即“等效串联电阻”.理想的电容自身不会有任何能量损失,但实际上,因为制造电容的材料有电阻,电 ...
- Redis数据库的使用场景介绍(避免误用Redis)
转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/122.html?1455854235 Redis 是目前 NoSQL 领域 ...
- Atitit 发帖机实现(4 )- usbQBM1601 gui操作标准化规范与解决方案attilax总结
Atitit 发帖机实现(4 )- usbQBM1601 gui操作标准化规范与解决方案attilax总结 1.1. 根据gui的类型使用不同的gui调用api1 1.2. Script化1 1.3. ...