写点什么

JVM 专题 01- 类加载机制详解

用户头像
JustRunning
关注
发布于: 1 小时前
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()防范

  • JNDISPI接口

  • OSGI 热部署

  • JDK9引入的模块化系统(下步专题梳理)

3.3 自定义类加载器实现

基本步骤:

  • 继承抽象类java.lang.ClassLoader

  • 重写 findClass 方法逻辑


示例:自定义一个 Classloader,加载一个 Hello.xlass 文件,执行 hello 方法,此文件内容是一个 Hello.class 文件所有字节(x=255-x)处理后的文件。文件群里提供。


public class Xlass2ClassLoader extends ClassLoader{
public static void main(String[] args){ Xlass2ClassLoader xlass2ClassLoader = new Xlass2ClassLoader(); try { Class helloClass = xlass2ClassLoader.findClass("Hello.xlass"); Method helloMethod = helloClass.getDeclaredMethod("hello"); helloMethod.invoke(helloClass.newInstance()); } catch (Exception e) { e.printStackTrace(); } }
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { DataInputStream in = new DataInputStream(Xlass2ClassLoader.class.getClassLoader().getResourceAsStream(name)); try { int length = in.available(); byte[] decodeBytes = new byte[length]; for (int i=0;i<length;i++){ decodeBytes[i]=(byte)(255-in.readByte()); } in.close(); return defineClass("Hello",decodeBytes,0,length); } catch (IOException e) { e.printStackTrace(); } return super.findClass(name); }}
复制代码

3.3 关于 ClassLoader 接口说明

除了启动类加载器之外,其他所有加载器的父类,主要方法如下表清单:


发布于: 1 小时前阅读数: 6
用户头像

JustRunning

关注

还未添加个人签名 2018.03.03 加入

还未添加个人简介

评论

发布
暂无评论
JVM专题01-类加载机制详解