Skip to content

Latest commit

 

History

History
302 lines (224 loc) · 15.7 KB

File metadata and controls

302 lines (224 loc) · 15.7 KB
title Java的字节码
date 2018-11-21 02:56:52 -0800
tags
字节码
categories
Java

💠

💠 2026-06-28 18:20:23


字节码以及类加载

相关示例代码


书籍

  • 深入理解Java虚拟机 周志明
  • Java 虚拟机字节码 从入门到实战 吴就业

编译优化

由源文件 *.java 编译成 *.class 文件这个过程中做的调优

类中定义的常量 如果是字面值, 其他引用这个常量的地方编译后会被替换成字面值, 该常量属性的 get 方法也是直接返回字面值
字面值就是无需运算的值, 而不是表达式 例如 final int num = 2; 反例 final int num = 3>2?1:2;

JIT 即时编译

目前有三种:C1 C2 Graal

类型 JVM参数 特点
C1 -client 编译耗时短
C2 -server 编译耗时长执行效率好需要收集运行期profile信息来辅助编译也就是PGO
Graal

注意 自JDK8起默认开启分层编译C1 C2混用 -client -server参数无效

Graal Compiler
Deep Dive Into the New Java JIT Compiler – Graal


字节码

参考: 学会阅读Java字节码 参考: 字节码增强技术探索

字节码是程序的中间表达形式,源码和机器码之间的产物 字节码是由源文件执行javac产生的

某些高级语言特性(语法糖)在字节码中会进行简化,例如循环结构,会转换成为分支指令

  • 每个操作都由一个字节表示,因此叫做字节码

  • 字节码是一种抽象表示方法 字节码进一步编译得到机器码

  • javap -c -p class文件 反编译字节码文件,-p 能看到私有属性

    • 输出所有的属性以及类的定义信息
    • 静态块
    • 构造方法
    • 方法信息
    • 静态属性信息
    • 静态方法信息

字节码相关框架

Apache bcel

asm

使用场景:

  • Java Lambda表达式
  • Arthas

javassist

raphw/byte-buddy: Runtime code generation for the Java virtual machine.

后端 - 打开java语言世界通往字节码世界的大门——ASM字节码操作类库 - 京东云技术新知 - SegmentFault 思否

使用场景:

  • Mockito 3+ 默认引擎已切到 Byte Buddy,百万级测试日运行。 @MockBean 底层就是 Byte Buddy
  • 用 Byte Buddy Agent 在 JVM 启动时拦截 DAO/Service 关键方法; 方法入参里读取 userId → 取灰度配置 → 改写 SQL 表后缀(_v1/_v2);
  • Agent 拦截 PreparedStatement.executeQuery();对 SQL 做 FNV-1a 哈希 → 本地 Caffeine 计数; QPS > 阈值 → 直接抛 自定义异常 → 前端降级缓存; 熔断窗口 5s → 自动恢复。
  • Byte Buddy Agent 统一拦截 java.sql.PreparedStatement org.springframework.web.method.HandlerMethod 美团 「Cat 无侵入监控」 基于 Byte Buddy,单机 5W+ QPS 采样
  • 订单价格 动态计算规则 Groovy 脚本维护在 配置中心; 用 ASM 在 运行时把脚本编译成 字节码类 → 注入 Spring 容器; 方法签名不变,内部逻辑已换; 灰度发布脚本即可,JVM 0 重启。 案例:拼多多 「动态定价引擎」 每周上线 200+ 规则
  • API 返回值脱敏: Byte Buddy Agent 拦截 JSON 序列化前一刻;反射字段 → 正则脱敏 → 继续序列化

常量池

常量池是为类文件中的其他常量元素提供快捷访问方式的区域。对于JVM来说常量池相当于符号表 java中的堆、栈、常量池以及String类型的两种声明 - LeonNew - 博客园

  • javap -v class文件 输出很多额外信息,# 开头的就是常量池信息 图

常量池中主要存放两大类常量:

  1. 字面量 (Literals):如文本字符串("hello")、声明为 final 的常量值、基本数据类型的值。
  2. 符号引用 (Symbolic References):包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

Java 架构中实际上有三个处于不同阶段、不同位置的“常量池”:

常量池类型 存在位置 存在生命周期 核心作用
Class 文件常量池
(静态常量池)
磁盘上的 .class 文件中 编译期生成,存放在磁盘 存储该类编译后的字面量与符号引用。
运行时常量池
(Runtime Constant Pool)
内存中的元空间 (Metaspace) 运行期,类加载后存在 类加载时,JVM 将上述静态常量池读入内存,并将符号引用解析替换为直接引用(真实的内存地址)。
字符串常量池
(String Constant Pool)
内存中的 (Heap) 运行期,全局共享 专门为了全局复用 String 字面量,避免重复创建对象造成的内存浪费。

在阅读老博客或旧书时,关于常量池的位置有很多自相矛盾的描述,请以现代 JVM 行为为准:

  • Java 6 及以前:运行时常量池和字符串常量池都在永久代 (PermGen) 中。如果大量使用 intern() 很容易导致 java.lang.OutOfMemoryError: PermGen space
  • Java 7:将字符串常量池静态变量从永久代移出,放到了 Java 堆 (Heap) 中。
  • Java 8 及以后:彻底废除永久代,引入元空间 (Metaspace)(直接使用物理内存)。运行时常量池移入元空间;而字符串常量池依然保留在 Java 堆 (Heap) 中。

类加载机制

  • 类的生命周期
    • 加载 Loading -> 验证 Verification -> 准备 Preparation -> 解析 Resolution -> 初始化 Initialization -> 使用 Using -> 卸载 Unloading
    • 验证、 准备、 解析,统称为 链接 Linking

类加载器

参考: 一文带你深扒ClassLoader内核,揭开它的神秘面纱! 深入源码,举例清晰

深入分析Java类加载器原理本文分析了双亲委派模型的实现原理,并通过代码示例说明了什么时候需要实现自己的类加载器以及如何 - 掘金

双亲委派模型(parents delegation model) 实现代码:java.lang.ClassLoader#loadClass(java.lang.String, boolean) 其工作原理是,如果一个类加载器收到了类加载请求(只讨论首次加载,已经加载过的会走缓存), 它并不会自己先去加载,而是委托给父类的加载器去执行
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载

  • Java平台经典类加载器层级:
    1. BootStrap ClassLoader 根(引导)加载器:通常在VM启动后不久就实例化,最顶层的加载类,主要加载 核心类库 并且不做验证工作
      • 加载目录 %JRE_HOME%\lib 下的rt.jar、resources.jar、charsets.jar 和 class 文件
    2. Extendsion ClassLoader 扩展类加载器:加载安装时自带的标准扩展,一般包括安全性扩展
      • 加载目录 %JRE_HOME%\lib\ext 下的 jar 包和 class 文件。
    3. Application ClassLoader 应用或系统类加载器:应用最广泛的类加载器,负责加载应用类(当前应用的 classpath 的所有类)
    4. 自定义ClassLoader 自定义类载器

注意:

  1. 例如在读取类路径下文件时,可以通过 classA.getClassLoader().getResourceAsStream("app.properties") 但是如果类classA对象是由 BootStrap 类加载器加载的, getClassLoader() 将返回 null
  2. 当出现jar包多版本时,先加载了其中一个版本就不会加载另一个版本,而这个加载顺序往往是由操作系统的文件排序决定的 相关案例

特殊场景

JDBC,JNBI,Tomcat 等

JDBC

例如 DriverManager.getConnection(); 创建JDBC连接, java.sql.DriverManager#loadInitialDrivers 静态代码块中实现了驱动类的加载,同时 DriverManager位于 rt.jar, 会被BootStrap 加载,但是驱动实现类通常在外部目录,第三方的类不能被根加载器加载。

JDBC中通过引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)的方式破坏了双亲委派原则

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

Tomcat

参考: 图解Tomcat类加载机制

  • CommonClassLoader Tomcat最基本的类加载器,加载路径/common/*中的class可以被Tomcat容器本身以及各个Webapp访问;
  • CatalinaClassLoader Tomcat容器私有的类加载器,加载路径/server/*中的class对于Webapp不可见;
  • SharedClassLoader 各个Webapp共享的类加载器,加载路径/shared/*中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader 各个Webapp私有的类加载器,加载路径/WebApp/WEB-INF/*中的class只对当前Webapp可见;

为了实现不同Tomcat容器间的隔离, WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用对应一个WebApp类加载器WebAppClassLoader,每一个JSP文件对应一个Jsp类加载器。

WebApp类加载器就为了类隔离而违背了双亲委派模型,仅自身负责加载类,不向上传递

JDK9 jigsaw

模块化(jboss modules、osgi、jigsaw)

在JDK9中,整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。

    Class<?> c = findLoadedClass(cn);
    if (c == null) {
        // 找到当前类属于哪个模块
        LoadedModule loadedModule = findLoadedModule(cn);
        if (loadedModule != null) {
            //获取当前模块的类加载器
            BuiltinClassLoader loader = loadedModule.loader();
            //进行类加载
            c = findClassInModuleOrNull(loadedModule, cn);
        } else {
            // 找不到模块信息才会进行双亲委派
                if (parent != null) {
                c = parent.loadClassOrNull(cn);
                }
        }
    }

加载和连接

参考: 第七章.虚拟机类加载机制

加载

  • 这个过程就是读取字节码文件,创建一个字节数组装在这些内容,加载结束后这个对象还不能直接调用

连接

  • 加载完成后,类必须连接起来,分为三步:验证,准备,解析。
    • 验证:
      • 验证文件的合理性,完整性检查,检查常量池,方法的字节码检查。主要的:
      • 是否所有方法都遵守访问控制关键字的限定
      • 方法调用的参数个数和静态类型是否正确
      • 确保字节码不会试图滥用堆栈
      • 确保变量使用之前被正确初始化了
      • 检查变量是否仅被赋予恰当类型的值
    • 准备:
      • 分配内存,准备初始化类中的静态变量,但不会现在就初始化,也不会执行任何VM字节码
    • 解析:
      • 促使VM检查类文件中所引用的类型是不是都是已知的类型。如果有运行时有未知的类型,那又要引发一次类加载过程
      • 当需要加载的类全部加载解析完毕后,VM就可以初始化最初那个加载的类了。
      • 这时所有的静态变量都可以进行初始化,所有静态代码块都会运行,这一步完成后,类就能使用了

方法句柄

主要用于反射 用到再学

图


Agent

JDK: Interface Instrumentation

Guide to Java Instrumentation
Java Agent 探针技术

Java Agent 主要有以下功能

  • Java Agent 在加载 Java 字节码之前拦截并对字节码进行修改;
  • Java Agent 在 Jvm 运行期间修改已经加载的字节码;

Java Agent 的应用场景

能力 案例
IDE的调试功能 Eclipse、IntelliJ IDEA ;
热部署功能 JRebel、XRebel、spring-loaded;
各种线上诊断工具 Btrace、Greys, Arthas;
各种性能分析工具 Visual VM、JConsole 等;
全链路性能检测工具 Skywalking、Pinpoint等;

agent线程池监控


反编译

JD
https://varaneckas.com/jad/
Java-Class-Viewer
classpy


热部署

通过替换 class 实现不停机热更新

Spring hot swapping

  1. Instrumentation
  2. 自定义类加载器
  3. OSGI 热插拔接口

Instrumentation 新功能 基于Java Instrument的Agent实现 Java 5 特性 Instrumentation 实践 java组件中的热插拔(osgi) agentmain 方式

Java 类的热替换 —— 概念、设计与实现 - 时空穿越者 - 博客园

相关项目:

game-hot-update | 游戏服务器之Java热更新 groovy hotswap demo

Java系列 | 远程热部署在美团的落地实践未开源