JVM(1) 字节码、类加载器
2024-09-29 14:32:52 # Language # Java

1. 字节码

1.1 字节码的组成

基础信息:魔数(0xCAFEBABE)、字节码文件对应的Java版本号、访问标识(public final…)、父类和接口索引

常量池:保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用

如果变量名和值相同,只会在常量池存储一份

字段:当前类或接口声明的字段信息

方法:当前类或接口生命的方法信息,字节码指令

属性:类的属性,比如源码的文件名、内部类的列表等

i++与++i的区别

  • i++会先将i赋值到操作数栈,直接在局部变量表+1
  • ++i会先在局部变量表+1,再赋值到操作数栈

1.2 常用工具

  • 查看字节码:jclasslib
  • 查看字节码:javap -v {字节码文件名称}
  • 阿里arthas
    • 启动:java -jar arthas-boot.jar
    • 加载类的字节码文件:dump {类的全限定名}
    • 反编译:jad {类的全限定名}

2. 类的生命周期

加载 —> 连接(验证、准备、解析) —> 初始化 —> 使用 —> 卸载

加载:根据类的全限定名把字节码文件的内容加载并转换成合适的数据放入内存中,存放在方法区(InnerKlass)和堆上(Class)

连接-验证:魔数、版本号等验证

连接-准备:为静态变量分配内存并设置初始值

连接-解析:将常量池中的符号引用(编号)替换为直接引用(内存地址)

初始化:执行静态代码块和静态变量的赋值

  • 静态变量的定义如果使用了final关键字,这类变量会在准备阶段直接进行初始化(除非要执行方法)
  • 直接访问父类的静态变量,不会触发子类的初始化。子类的初始化 clinit 调用之前,会先调用父类的 clinit 初始化方法。
  • 添加 -XX:+TraceClassLoading 参数可以打印出加载并初始化的类

3. 类加载器

3.1 类加载器的分类

虚拟机底层实现:位于虚拟机源码,使用C++实现,保证程序运行时的基础类被正确的加载

Java:JDK中默认提供或自定义,所有Java中实现的类加载器都继承自ClassLoader抽象类

JDK8及之前版本 JDK9及之后的版本(模块化 jmod)
虚拟机底层实现 启动类加载器Bootstrap
Java实现 启动类加载器 (jdk.internal.loader.ClassLoaders)
BootClassLoader继承自BuitinClassLoader实现从模块中找到字节码文件
仍然无法通过java代码获取到(null),保持了统一
扩展类加载器Extension (sun.misc.Launcher.java) 平台类加载器 (jdk.internal.loader.ClassLoaders)
继承自BuitinClassLoader,其存在更多的是为了与老版本的设计兼容,没有特殊逻辑
应用程序类加载器Application (sun.misc.Launcher.java) 应用程序类加载器 (jdk.internal.loader.ClassLoaders)
继承自BuitinClassLoader

arthas

  • classloader:查看类加载器的继承树、urls、类加载信息…
    • -l:查看类加载器hash
    • -c {hash}:查看类加载器的加载路径
    • -t:查看类加载器间的父子关系
  • sc -d {类的全限定名}:查看 JVM 已加载的类信息

启动类加载器:由Hotspot虚拟机提供,C++编写,默认加载/jre/lib下的类文件

  • 如何通过启动类加载器加载用户jar包
    • 放在jre/lib下进行扩展(不推荐,文件名可能不匹配出错)
    • 使用参数(推荐): -Xbootclasspath/a:{jar包目录/jar包名}

image-20240927163342213

扩展类加载器:JDK中提供,默认加载/jre/lib/ext下的类文件

  • 如何通过扩展类加载器加载用户jar包

    • 放在jre/lib/ext下进行扩展(不推荐)

    • 使用参数(推荐): -Djava.ext.dirs={jar包目录},由于这种方式会覆盖原始目录,可使用;{原始目录}追加

应用程序类加载器:加载classpath下的类文件

3.2 双亲委派机制

定义:当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载

作用:保证类加载的安全性(避免恶意替换核心类库)、避免重复加载

类加载器间的父子关系并不是继承关系,而是通过 private final ClassLoader parent; 来体现

img

3.3 打破双亲委派机制

https://www.cnblogs.com/hollischuang/p/14260801.html

自定义类加载器

自定义类加载器并重写loadClass方法,就可以去除双亲委派的代码(Tomcat就通过这种方式实现应用之间类的隔离)

  • 如果不希望破坏双亲委派并想自定义类加载器,重写findClass方法即可

两个自定义类加载器加载相同限定名的类不会冲突,只有相同类加载器+相同类限定名才会被认为是同一个类

线程上下文类加载器

利用上下文类加载器加载类,比如JDBC, JNDI等(SPI机制)

SPI是JDK内置的一种服务提供发现机制

  • 在classpath路径下的META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现类的全限定名
  • SPI使用线程上下文中保存的类加载器进行类的加载,这个类加载器一般是AppClassLoader

image-20240929135608595

讨论:通过线程上下文类加载器是否真正打破了双亲委派机制?

  • 打破了:由启动类加载器加载的类,委派应用程序类加载器加载类的方式,打破了双亲委派机制
  • 没有打破:JDBC只是在DriverManager加载完之后,通过初始化阶段出发了驱动类的加载,类的加载依然遵循双亲委派机制

Osgi框架的类加载器

历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载。OSGi还是用类加载器实现了热部署的功能(不停机动态更新字节码文件到内存)

arthas热部署

1
2
3
4
5
6
# 反编译源代码
jad --source-only {类全限定名} > {目录/文件名.java}
# 编译修改过的代码
mc -c {类加载器的hashcode} {目录/文件名.java} -d {输出目录}
# 加载新的字节码
retransform {class文件所在目录/xxx.class}

程序重启后,字节码文件会恢复,除非将class文件放入jar包中进行更新

使用retransform不能添加方法或字段,也不能更新正在执行中的方法