JVM 类加载机制和字节码技术

1. 简述

典型的Java程序执行流程如下:

  1. 我们在本地编写完Java源程序;
  2. IDE自动帮我们编译成.class文件(也可以手动通过javac命令编译),然后打包成jar包或者war包;
  3. 接着,执行java -jar命令或直接部署到web容器中来运行程序;
  4. 运行时,OS会启动一个JVM进程,JVM会采用类加载器将各种.class文件中包含的Java类加载到内存中;
  5. 最后,JVM基于自己的字节码执行引擎,来执行加载到内存中的那些类。

2. 类加载机制

2.1 类加载器

类加载器可以大致划分为以下三类:

  • Bootstrap ClassLoader: 主要负责加载 JDK 安装目录下的核心类库(比如/lib目录下的类),这些核心类库是JVM运行时自身需要用到的。开发者不能直接在Java程序中使用
  • Extension ClassLoader:主要负责加载 JDK 安装目录下的扩展类库(比如/lib/ext目录下的类),这些扩展类库是JDK按照功能进行模块划分的,一般也是Java程序运行所必需的。比如使用maven引入的第三方包
  • Application ClassLoader:负责加载用户类路径(classpath)所指定的类,可以简单的理解成负责加载用户自己开发的Java类。
  • 除了上述提供到三种类加载器外,开发者也可以自定义类加载器,根据自己的需求去加载类。

2.2 双亲委派机制

JVM的类加载器是有亲子层级结构的,层级结构如下图:

当我们的类加载器需要加载一个类时,首先会委派给自己的父类加载器去加载,最终传到到顶层 的类加载器去加载;如果某个父类加载器发现在自己负责的范围内并没有找到这个类,就会下推加载权力给自己的子类加载器。

以上图为例:

  1. 当Application ClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器Extension ClassLoader去完成;
  2. 当Extension ClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给Bootstrap ClassLoader去完成;
  3. 如果Bootstrap ClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用Extension ClassLoader来尝试加载;
  4. Extension ClassLoader也加载失败,则会使用Application ClassLoader来加载,如果Application ClassLoader也加载失败,则会报出ClassNotFoundException异常。

双亲委派机制的主要优点有:

  1. 避免重复加载。如果一个类已经被父类加载器加载过了,那么子类加载器就不需要再次加载这个类,从而避免了重复加载。

  2. 保护Java核心类库的安全性。Java核心类库由启动类加载器加载,它们在整个Java应用程序中都是唯一的。通过双亲委派机制,可以确保Java核心类库的安全性,防止应用程序意外地修改这些类库。

  3. 保证类的一致性。通过双亲委派机制,可以保证同一个类在不同的类加载器中只有一个版本,从而避免类的版本冲突。

双亲委派机制的缺点是可能会导致类加载器的性能较差,因为每个类的加载都需要通过双亲委派机制进行多次委派和查询。但是在实际应用中,这种性能影响通常是可以接受的。

2.3 完整流程

类从.class二进制数据被加载到 JVM 内存中开始,到卸载出内存为止,它的整个生命周期包括:

加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading),共7个阶段。

  • 加载阶段: 很简单,当程序执行到需要的类时,JVM就会通过类加载器 将其加载到内存中.
  • 验证阶段:根据Java虚拟机规范,需要对加载进来的“.class”文件的内容进行校验,包括验证文件格式、元数据、字节码、符号引用等各种信息,以确认是否符合指定的规范
  • 准备阶段:主要是为类及其静态字段分配内存,并将其初始化为默认值
  • 解析阶段:实际上是把类的符号引用替换为直接引用的过程
  • 初始化阶段:之前说过,JVM会在准备阶段给类的静态字段分配空间和默认值。而在初始化阶段,就会正式执行类的初始化代码,对类进行初始化操作
public class ReplicaManager {
    public static int flushInterval = Configuration.getInt("replica.flush.interval");
    public static Map<String,Replica> replicas;

    static {
        loadReplicaFromDish():
    }

    public static void loadReplicaFromDish(){
        this.replicas = new HashMap<String,Replica>();
    }
}

对于flushInternal变量,我们通过一个getInt方法从配置中获取值并进行赋值,这个赋值动作在准备阶段是不会执行的,而是在初始化阶段执行。另外,对于static静态代码块,也是在这个阶段执行的

在初始化阶段,如果JVM初始化某个类时,发现其父类还没有初始化完成的话,会首先去加载其父类,加载策略就是上一节提到的双亲委派机制。

  • 卸载阶段:就是当对象不再需要使用时,JVM需要进行垃圾回收

3. tomcat类加载机制

Tomcat的类加载体系如下图,蓝色部分是Tomcat继承Application ClassLoader实现的自定义类加载器:

 

Tomcat的类加载器主要有以下几种:

  1. Bootstrap类加载器。Bootstrap类加载器是Java虚拟机的启动类加载器,负责加载Java核心类库。在Tomcat中,Bootstrap类加载器的作用和Java标准类加载机制中是一致的。

  2. System类加载器。System类加载器也被称为应用程序类加载器,它负责加载Web应用程序中的类和第三方类库。在Tomcat中,System类加载器会为每个Web应用程序创建一个独立的类加载器实例,以避免不同Web应用程序之间的类冲突。

  3. Common类加载器。Common类加载器是Tomcat中的一个特殊类加载器,它负责加载Tomcat本身的类和第三方共享类库。在Tomcat中,所有Web应用程序都可以共享Common类加载器加载的类。

  4. Webapp类加载器。Webapp类加载器是Tomcat中最重要的类加载器之一,它负责加载Web应用程序中的类和资源。在Tomcat中,每个Web应用程序都有自己独立的Webapp类加载器实例。

Tomcat的类加载器按照一定的顺序进行委派和查询,保证了类的一致性和安全性,并避免了重复加载。具体来说,Tomcat的类加载器委派和查询的顺序如下:

  1. Webapp类加载器首先会尝试加载Web应用程序中的类和资源。

  2. 如果Webapp类加载器无法加载某个类或资源,那么就会委派给父类加载器System类加载器来加载。如果System类加载器仍然无法加载,就会继续委派给父类加载器Common类加载器来加载。

  3. 如果Common类加载器仍然无法加载,就会继续委派给Bootstrap类加载器来加载。

通过这种方式,Tomcat的类加载器可以确保Web应用程序中的类和资源都能够正确地被加载和使用,并且能够避免类的重复加载和冲突。

因此,在Tomcat中,Webapp类加载器的委派过程是在双亲委派机制的原则进行定制的如果父类加载器能够直接加载请求的类或资源,Webapp类加载器就会将加载请求直接交给父类加载器,而不会继续向上委派。

 4. 字节码技术

我们知道,我们编写的 Java 代码都是要被编译成字节码后才能放到 JVM 里执行的,而字节码一旦被加载到虚拟机中,就可以被解释执行。字节码文件(.class)就是普通的二进制文件,它是通过 Java 编译器生成的。而只要是文件就可以被改变,如果我们用特定的规则解析了原有的字节码文件,对它进行修改或者干脆重新定义,就可以改变代码行为了。

可在程序中动态生成.class文件,还可以动态修改.class文件

ASM

ASM 是它们中最强大的一个,使用它可以动态修改类、方法,甚至可以重新定义类,连 CGLib动态代理 底层都是用 ASM 实现的。但是它使用起来挺难的,所以我们使用javassist来实现。

javassist

ASM使用起来有点麻烦,所以我们使用javassist来做,虽然性能较ASM略低,但是它使用起来简单

示例

使用javassist动态创建一个User类

//使用java字节码技术创建字节码
public class Test002 {

	public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException {
		ClassPool pool = ClassPool.getDefault();
		// 1.创建user类
		CtClass userClass = pool.makeClass("com.itmayiedu.entity.User");
		// 2.创建name 和age属性
		CtField nameField = CtField.make("	private String name;", userClass);
		CtField ageField = CtField.make("	private Integer age;", userClass);
		// 3.添加属性
		userClass.addField(nameField);
		userClass.addField(ageField);
		// 4.创建方法
		CtMethod nameMethod = CtMethod.make("public String getName() {return name;}", userClass);
		// 5.添加方法
		userClass.addMethod(nameMethod);
		// 6.添加构造函数
		CtConstructor ctConstructor = new CtConstructor(
				new CtClass[] { pool.get("java.lang.String"), pool.get("java.lang.Integer") }, userClass);

		ctConstructor.setBody("	{ this.name = name; this.age = age; }");
		userClass.addConstructor(ctConstructor);

		// 生成class文件
		userClass.writeFile("F:/test");
	}

}

 反编译JD-GUI

编译后的.class文件可以通过JD-GUI工具来反编译查看是否正确

参考博客:JVM基础(1)——JVM类加载机制 | 山海 | 专注分布式系统架构与设计

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巴中第一皇子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值