JVM 专题 01- 类加载机制详解
概述:Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 对象的过程,被称作 JVM 类加载机制。与其他编译期就进行连接的语言不同,虽然增加了额外开销和预编译难度,但极大提升了应用的扩展性和灵活性。下面我们从加载流程、类加载时机、双亲委派机制以及自定义实现一个类加载器。
1、类加载流程
从字节码文件加载到内存,之后触发验证、解析、初始化,最终将类加载到方法区中,供所有线程共享并可以直接当 java 对象使用。
其中各个流程节点的核心任务划分如下:
加载:根据路径规则查找到相应的 class 文件并读取写入到内存中。
验证:确保 Class 文件字节流包含信息符合 JVM 要求,并保证类的正确性,不危害虚拟机安全,主要分为文件格式、元数据、字节码以及符号引用验证。
准备:为类变量分配内存等(类变量方法区,实例变量堆中)。
解析:常量池内的符号引用替换成直接引用的过程,包含类或接口解析、字段解析、方法解析、接口方法解析。
初始化:执行类构造器
<clinit>()
方法的过程,主要包含所有类变量赋值和静态语句块的。
2、类加载时机(初始化)
2.1 类初始化触发
JVM 规范中并未明确第一阶段“加载”时机,但严格限制了有且只有六种必须立即对类进行初始化的场景(而其前面的加载-验证-准备自然需要同时完成):
虚拟机启动时,初始化用户指定的主类,即启动执行的 main 方法所在类。
遇到
new/getstatic/putstatic/invokestatic
四个字节码指令new
: 实例化对象getstatic/putstatic
:设置一个类型的静态字段(final 修饰的除外)invokestatic
:调用一个类型的静态方法子类初始化时,父类如果没初始化会触发父类初始化
接口定义了
default
方法(JDK8 新加入的默认方法关键字),如果该接口实现类发生初始化,该接口就必须先触发接口初始化。反射(
java.lang.reflect
)调用某个类时JDK7 动态语言支持引入
java.lang.invoke.MethodHandle
的调用
这六种场景行为,通常称为主动引用。
2.2 接口初始化
除了实现类外,接口同时也需要加载,接口加载有以下几个特点:
一个类初始化时,要求其父类全部已经初始化
一个接口初始化时,并不要求其父接口全部完成初始化,只有真正使用父接口时(引用接口中定义的常量)才触发。
2.3 不会初始化
加载时机除了必须初始化的场景外,还有以下几种并不需要初始化但容易混淆的场景:
通过子类引用父类静态字段,只会触发父类初始化,不会触发子类初始化
定义对象数值,不会触发该类的初始化
常量编译期间会存入调用类的常量池中,本质并没有直接应用定义常量的类,不会触发所在常量类初始化
通过类名获取 Class 对象,不会触发
Class.forName 加载指定 initialize 为 false 时,需要时才会触发。
ClassLoadder 默认的 loadClass 方法,也不会触发初始化(加载了,但是不会初始化)
3、类加载器(Class Loader)
官方描述,通过一个类的全限定名来获取描述该类的二进制字节流的实现动作,目前按 JVM 按层级分为三层。
3.1 各层加载器的职责内容
启动类加载器(Bootstrap Class Loader)
路径:
%JAVA_HOME%/lib
或者通过-Xbootclasspath
启动参数指定,而且需要 jvm 能够识别(按照文件名识别,如 rt.jar/tools.jar,名称不符合规范,即使放路径下也不会被加载)。内容:java 核心基础类。
无父加载器,且该加载器无法被程序直接引用。
出于安全考虑,只加载包名为
java/javax/sun
等开头的类扩展类加载器(Extension Class Loader)
路径:默认
%JAVA_HOME/lib/ext%
或 java.ext.dirs 系统属性指定。扩展机制支持开发者直接使用该加载器来加载 Class 文件
应用程序加载器(Application Class Loader)
路径:环境 classpath 或者系统属性 java.class.path 指定路径下。
父类加载器是扩展类加载器。
通过 ClassLoader#getSystemClassLoader()方法可以获取该类加载器
自定义类加载器(意义)
隔离加载类,如中间件的 jar 包和应用程序 jar 冲突问题
修改类加载方式+扩展加载源
自定义字节码加载逻辑,如对字节码进行加密,用自定义加载器实现解密,防止源码泄露
3.2 双亲委派模型
3.2.1 概念
JVM 的角度来看,系统只存在两种不同的类加载器(双亲):
启动类加载器:由 C++实现,是 JVM 自身的一部分。
其他所有的类加载器:java 语言实现,独立存在于虚拟机之外,且全部继承自
java.lang.ClassLoader
3.2.2 流程
一个类加载器收到类加载请求时,首先,把这个请求委派给父类加载器去完成,层层委托,最终到最顶层的启动类加载器,只有当父加载器反馈无法加载,子加载器才会尝试自行加载。
3.2.3 作用
避免类的重复加载
保护程序安全,防止核心 API 被篡改
沙箱安全机制:保证对 Java 核心源码的保护
3.2.4 破坏双亲委派模型
ClassLoader
兼容而提供的findClass()
防范JNDI
的SPI
接口OSGI
热部署JDK9
引入的模块化系统(下步专题梳理)
3.3 自定义类加载器实现
基本步骤:
继承抽象类
java.lang.ClassLoader
重写 findClass 方法逻辑
示例:自定义一个 Classloader,加载一个 Hello.xlass 文件,执行 hello 方法,此文件内容是一个 Hello.class 文件所有字节(x=255-x)处理后的文件。文件群里提供。
3.3 关于 ClassLoader 接口说明
除了启动类加载器之外,其他所有加载器的父类,主要方法如下表清单:
版权声明: 本文为 InfoQ 作者【JustRunning】的原创文章。
原文链接:【http://xie.infoq.cn/article/e5a9b368dd1172fd60c2742ab】。未经作者许可,禁止转载。
评论