ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java字节码入门:用javap和jclasslib读懂.class文件

Java字节码入门:用javap和jclasslib读懂.class文件 很多Java开发者写了几年代码天天跟.java源码打交道但一说到.class文件里的字节码就觉得那是JVM专家或者框架作者才需要碰的东西。直到有一次我帮同事排查一个诡异的线上接口耗时问题——源码翻来覆去看了好几遍逻辑都正常数据库也没慢查询最后没办法把编译后的class反汇编一看才发现问题出在一个看似无害的字符串拼接上它在字节码层面搞出了一堆多余的对象创建。从那以后我就养成了一个习惯遇到想不通的Java问题先看一眼字节码再说。字节码这个东西说白了就是Java源码编译后的中间产物JVM不认你的Java语法但它认这套指令集。学查看字节码不是让你去背指令手册而是要建立一种“从源码到JVM执行中间层”的直觉——很多语法糖是怎么实现的、为什么某些代码性能差、框架的动态代理到底做了什么答案全藏在里面。这篇文章就带你从零开始把javap、jclasslib这些工具用熟然后自己动手拆解一段真实代码的字节码最后聊几个我实际踩过的坑。不管你是准备面试、排查线上问题还是想搞懂Spring和MyBatis的原理这篇都适合你花一个小时过一遍。1. 为什么要折腾字节码——先搞懂这东西值不值得学1.1 字节码到底是个什么玩意一个.java文件经过javac编译后得到的是.class文件这文件里面装的不是机器码而是JVM能够识别的一套二进制指令业界习惯叫它字节码Bytecode。之所以叫字节码是因为每条指令在class文件里基本上用一个字节8位)来表示操作码后面可以跟零个或多个操作数。打个比方源码是你的“设计图纸”字节码是“施工图纸”机器码是“盖好的房子”。JVM读取施工图纸再通过解释执行或者JIT即时编译把字节码翻译成当前操作系统和CPU架构真正能跑的机器码。所以同一个.class文件在Windows、Linux、macOS上都能跑这正是Java“一次编写到处运行”的底气——字节码是中间层跨平台的差异被JVM隔离掉了。那为什么要学看字节码因为它是唯一一个“既保留了源码结构信息、又完全暴露JVM执行细节”的层级。源码里一行a b c你根本看不到JVM是怎么把三个变量从局部变量表里load出来、执行加法、再存回去的。但看字节码所有细节一览无余。1.2 看得懂字节码能解决哪些实际问题先说最实际的四个场景这是我在工作中反复用到的第一排查性能问题。有些性能瓶颈在源码层面是看不出来的。比如字符串拼接源码里就一个加号但你去看字节码会发现Java编译器把它转换成了StringBuilder的多次append调用。如果在循环里直接拼字符串每次循环都new一个StringBuilder那GC压力就大了。这种问题不用字节码根本说不清楚。第二理解框架原理。Spring的AOP、MyBatis的Mapper代理、CGLIB动态代理它们底层都在做字节码生成和增强。你直接去看源码一头雾水但打开字节码一看发现代理类里多了一个被增强的方法调用链清晰可见。第三面试和考证。字节码相关的题目在Java面试里出现频率很高尤其是“String拼接的字节码原理”“动态代理的底层实现”“lambda表达式在字节码里长什么样”这些。很多所谓八股文你只要亲手看过一次字节码就能讲得比别人深一层而且不会被追问倒。第四读懂异常栈之外的真相。有时候源码和实际运行行为对不上比如编译器帮你加了泛型检查、自动拆装箱你看到的是编译器“自作主张”插入的代码这些在字节码里全是显式存在的。想搞清“为什么这里会抛NullPointerException”先看字节码比瞎猜高效得多。1.3 这篇文章适合谁以及你需要什么基础看这篇文章不需要你有深厚的JVM功底但至少得会写基本的Java语法知道类、方法、变量这些概念。如果你读过一点JVM内存模型比如栈、堆的基础知识理解起来会更轻松因为字节码的运行核心就是“栈”我们后面会从局部变量表和操作数栈入手。文章里所有命令和示例我都是在JDK 8到JDK 17上都验证过的你电脑上只要有JDK环境就能跟着实操。看到这里不妨现在就打开终端输入java -version确认一下环境没问题然后我们正式开始。2. 查看字节码的工具体系——从命令行到IDEA插件2.1 javapJDK自带的“原配”工具永远不要小看它很多人不知道JDK里自带了一个字节码查看工具叫javap。它不是什么高深的东西就是一个小命令行工具但功能极其扎实我日常90%的字节码查看需求都是靠它完成的。先看最基础的用法。假设你有一个类文件Demo.class执行javap Demo这会输出这个类的公开成员列表效果比你用IDE看类结构还精简。但这还不够想看到字节码指令必须加参数javap -c Demo-c参数会反汇编出每个方法的字节码指令这是最常用的。但要看到完整的细节——常量池、行号表、局部变量表——得加-v参数verbose啰嗦模式javap -v Demo-v输出非常长一个只有几行代码的类能输出几百行。很多人第一次看到就劝退了但恰恰是这个完整输出里藏着class文件结构的所有秘密。另外两个参数也很有用-p能显示私有成员默认只显示public/protected-s显示内部类型签名比如能看到泛型擦除后的真实类型。我把常用参数整理成了表格参数作用使用场景-c反汇编方法字节码指令最常用查看方法内部执行逻辑-v输出完整class信息查看常量池、行号表、StackMapTable等-p显示私有成员和方法查看private方法或字段-s显示内部类型签名确认泛型擦除后的真实类型-l显示行号表和局部变量表调试时定位源码行-constants显示static final常量值查看编译期常量注意javap后面的类名可以是全限定名也可以先cd到class文件所在目录再用简单类名。如果类在jar包里可以这样javap -cp myapp.jar -c com.example.Demo-cp指定类路径它会从jar包里找到对应类并反汇编。提示javap只能反汇编class文件不能直接把class还原成可读的Java源码。它干的是“反汇编”不是“反编译”这两者区别我们后面会细说。2.2 IDEA插件可视化阅读字节码的正确姿势命令行工具虽然万能但看复杂类时并不友好——一屏能装下的信息太少了还得来回滚动。IDEA里有两个插件我强烈推荐安装它们能大幅提升阅读速率。第一个是jclasslib Bytecode Viewer。装好之后编译一个类然后菜单栏选View - Show Bytecode With jclasslib就能用树形结构浏览这个类的常量池、字段、方法、属性。它的特点是信息结构化常量池里每个常量是啥类型、值是多少、被谁引用一目了然。对于初学者理解class文件格式用jclasslib比看javap -v的纯文本舒服很多。第二个是ASM Bytecode Viewer以前叫ASM Bytecode Outline。它不仅能看字节码指令还能同步显示如果用ASM框架生成同样的字节码代码应该怎么写。这对于研究字节码增强、写ASM代码的场景简直神器。你选中一个方法右边直接显示对应的ASM visitXxx调用序列照着抄都能写出代理逻辑来。我自己在IDEA里的习惯是这样日常快速看某个方法直接用javap命令因为不需要离开键盘需要仔细研究一个类的结构时打开jclasslib涉及动态代理或者字节码生成的框架开发就切到ASM Bytecode Viewer。三个工具各有侧重组合使用效率极高。2.3 别把“反编译”和“查看字节码”搞混了很多人会问能看class的内部代码吗IDEA里双击class文件直接就看到了近似源码的Java代码这不是更容易吗这里必须说清楚那个功能叫“反编译”工具本质是用Fernflower或CFR这类反编译器把字节码逆向还原成Java源码。它方便、易读但有两个问题第一反编译出来的代码和原始源码不一定一样。编译器会做很多转换比如switch的tableswitch/lookupswitch、字符串拼接、lambda脱糖反编译器只能尽力还原成最接近的形态。有些信息比如局部变量名、注释、格式化压根就被擦除了反编译是猜不回来的。第二反编译隐藏了JVM执行的真相。你看到的是“近乎源码”的逻辑但字节码里那个invokedynamic指令、那个StringBuilder对象创建、那个Integer.valueOf静态调用全都看不见了。而恰恰是这些细节才是查看字节码的核心价值。所以结论很明确想快速了解别人jar包里的逻辑用反编译想深入理解JVM是怎么执行这段程序的必须看原始字节码。两者不是替代关系是互补关系。下面的实战我们直接用最底层的javap -v逻辑来走一遍保证你看完能独立分析任意一个类的字节码。3. 手把手读懂class文件——常量池、方法表和指令3.1 class文件结构长什么样class文件虽然叫“文件”但它的本质是一串有严格格式的二进制流。JVM规范里规定了它的骨架先是魔数Magic Number固定为0xCAFEBABE——这4个字节的唯一作用就是告诉JVM“这是一个合法的class文件”不是随便改个后缀就能骗过去的。紧接着是次版本号和主版本号主版本号决定了这个class文件能在哪个版本的JDK上跑比如主版本52对应Java 861对应Java 17。如果版本跨度太大JVM会直接抛UnsupportedClassVersionError。再往后是常量池Constant Pool这是class文件里最庞大、最核心的区域可以理解成一张“符号表”类名、方法名、字段名、字符串字面量、类型描述符全都登记在这里。后面依次是访问标志Access Flags标记这个类是public还是final、是接口还是注解、本类索引this_class、父类索引super_class、接口索引表、字段表Fields、方法表Methods和属性表Attributes。这张表里的字段表和方法表记录的是类里每个字段、每个方法的“元信息”而方法真正的执行逻辑——字节码指令——存在方法表的Code属性里。我们看javap -c输出时看到的每一行指令就是从这些Code属性里提取出来的。3.2 常量池里的门道——它到底在省什么常量池是很多初学者最容易忽略的一层但恰恰最能体现JVM的设计智慧。直观感受一下假如源码里写了几十次System.out.println每次调用方法时难道指令里都要存一遍完整的java/io/PrintStream和println名字吗那class文件会膨胀得没法看。JVM的做法是把所有这些“可复用的符号信息”抽到常量池里统一登记比如把System.out对应的字段引用记成一个常量把println方法引用记成另一个常量。字节码指令里只需要引用常量池的索引号比如invokevirtual #12意思是“调用常量池里第12号常量所指的方法”。这样指令本身短小精悍信息全存在常量池一张表里需要的时候靠索引去查。用javap -v输出时你会看到常量池里每一行都带一个#编号后面紧跟常量类型如Methodref、String、Class、NameAndType。我刚开始啃的时候觉得这些编号乱糟糟后来总结了一个最省力的阅读法**从方法指令的索引号入手顺着编号去找对应的常量再由常量类型判断这个索引到底指向什么。**比如invokevirtual #16就去常量池找#16发现是个Methodref再顺着#16里的Class和NameAndType就能还原出它完整的方法签名。3.3 方法表里的核心逻辑操作数栈与局部变量表看javap -c反汇编出来的方法指令时一定要先理解JVM是基于栈的虚拟机。这句话什么意思就是说JVM执行字节码指令时不是像x86汇编那样直接操作寄存器而是通过一个“操作数栈”Operand Stack来垫数据指令从局部变量表里把变量推入栈load在栈上做运算add再把结果存回局部变量表store。这里有两个关键结构局部变量表Local Variable Table和操作数栈Operand Stack。局部变量表可以理解成方法内部的“草稿纸格子”编号从0开始this在实例方法里永远占0号位然后依次是方法参数和内部声明的局部变量。注意long和double因为64位要占两个连续slot其他类型包括引用类型占一个slot——这个细节在分析复杂方法时经常用到。操作数栈则是真正的“计算工作台”。以a b c为例字节码大致是这样iload_1把局部变量表第1个slot的int值压入操作数栈iload_2把第2个slot的int值压入操作数栈iadd弹出栈顶两个int相加把结果压回栈istore_3弹出栈顶int存入第3个slot整个过程像不像在厨房备菜一边是食材筐局部变量表一边是案板操作数栈指令就是你的手拿菜load、切菜iadd、装盘store。理解了这套模型字节码指令就好读多了——只要记住每条指令的“入栈”和“出栈”行为任何方法执行流程都能脑补出来。3.4 常见字节码指令分类速览字节码指令有200多个但日常看代码高频出现的不超过50个。我按用途分几类方便你对照类别代表指令作用加载/存储iloadistorealoadastore在局部变量表和操作数栈之间搬运数据。i代表inta代表引用类型常量入栈iconst_0bipushldcldc2_w把常量值压入操作数栈。小整数用iconst较大的用bipush/sipush字符串和复杂对象用ldc算术运算iaddisubimulidivirem弹出栈顶操作数做运算结果压回栈。浮点对应是fadd/dadd类型转换i2ld2ii2fint/long/double等类型之间互转对象操作newgetfieldputfieldgetstaticinvokevirtual创建对象、读写字段、调用方法方法调用invokevirtualinvokespecialinvokestaticinvokeinterfaceinvokedynamic五种方法调用指令后面专门说控制转移gotoifeqif_icmpnetableswitchlookupswitch实现if、for、switch等流程控制异常处理athrowmonitorentermonitorexit抛出异常、synchronized加锁/解锁返回ireturnareturnreturn方法返回按照返回类型有不同的return指令其中invoke系列指令是理解多态和Java新特性的关键。invokespecial一般用来调用构造函数、private方法和super方法invokevirtual用来做普通的实例方法分派就是多态的动态分派invokestatic调静态方法invokeinterface调接口方法。invokedynamic是Java 7引入、Java 8中lambda表达式的实现基础——很多面试爱问的“lambda在字节码层面是怎么实现的”答案就在这个指令上。4. 实战拆解——一段Java代码编译后的真实字节码4.1 准备样例代码与编译命令工具说再多不如亲手拆一个。我们写一个极简类把算术、字符串拼接、分支、循环都包含进去然后完整地走一遍字节码解读流程。先用记事本或IDEA创建一个Demo.javapublic class Demo { private int base 10; public int add(int a, int b) { int sum a b; if (sum this.base) { System.out.println(sum is large); } return sum; } }然后打开终端进入文件所在目录执行编译javac -g Demo.java-g参数很关键它告诉编译器在class文件里保留调试信息行号、局部变量名这样我们用javap -v时能看到源码行号和变量名学习阶段可读性会高很多。如果省略-g编译出的class文件也能运行但行号表可能缺失不过大多数情况下JDK默认会生成部分调试信息这里显式指定更稳妥。编译完会生成Demo.class。先跑一下完整输出javap -v Demo输出非常长我们分块来看。4.2 逐段解读javap -v输出输出的第一段是版本和常量池信息。开头会看到Classfile /path/to/Demo.class Last modified ... MD5 checksum ... Classfile 版本号第1个数字是次版本第2个是主版本然后是这段简化示意Constant pool: #1 Methodref #7.#18 // java/lang/Object.init:()V #2 Fieldref #3.#19 // Demo.base:I #3 Class #20 // Demo #4 Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 String #23 // sum is large #6 Class #24 // java/lang/System #7 Fieldref #6.#25 // java/lang/System.out:Ljava/io/PrintStream; ...是不是感觉很乱别慌我教你怎么读。每一行格式是“#编号 类型 具体内容”。比如#2 Fieldref #3.#19 // Demo.base:I意思是这是个字段引用指向#3这个类里的#19这个NameAndType常量注释里已经解析好了——Demo.base:I就是Demo类的base字段类型是Iint。JVM规范里用缩写表示类型I是intJ是longD是doubleF是float[是数组L类名;是引用类型。过了常量池后会看到字段表和方法表。以add方法为例输出大致是public int add(int, int); descriptor: (II)I flags: (0x0001) ACC_PUBLIC Code: stack3, locals4, args_size2 0: iload_1 1: iload_2 2: iadd 3: istore_3 4: iload_3 5: aload_0 6: getfield #2 // Field Demo.base:I 7: if_icmple 20 10: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 13: ldc #5 // String sum is large 15: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 18: goto 23 21: pop 22: pop 23: iload_3 24: ireturn先看最顶部的descriptor: (II)I——这是方法描述符括号里是参数类型两个int所以是II括号后面是返回类型I表示int。stack3, locals4说明这个方法运行时的操作数栈最大深度是3局部变量表有4个slot0号是this1号是a2号是b3号是sum。然后从左到右逐条看指令第0-2行iload_1把a入栈iload_2把b入栈iadd弹出两者相加结果压栈。第3行istore_3把相加结果弹出并存入局部变量表3号slot即sum。第4-6行iload_3把sum入栈aload_0把this引用入栈getfield #2弹出this获取其base字段值并压栈。此时栈顶是sum和this.base。第7行if_icmple 20——弹出栈顶两个int如果sum this.base就跳到偏移20。这个指令直接实现了if (sum this.base)的反向条件。第10-15行如果条件成立sum大于base先把System.out压栈再用ldc #5把字符串常量“sum is large”压栈最后invokevirtual #4调用println。第18行goto 23跳过println分支的收尾直接跳到偏移23。第21-22行这里居然是pop; pop。为什么会有这行其实是上一个版本的javac编译器生成的冗余清理代码在某些JDK版本里当if分支结束后栈里可能残留多余操作数编译器用pop清理以保持栈平衡。这个细节不用深究知道它不是业务逻辑就行。第23-24行iload_3把sum入栈ireturn返回它。看到这里你已经完成了一次完整的字节码阅读。你会发现源码里if条件在字节码里是反过来的源码写的是“如果大于就打印”字节码判断的是“如果不大于就跳过打印”。理解这种“跳转是反向的”规律以后看任何带分支的方法都会快很多。4.3 语法糖在字节码里长什么样光看一个简单加法和if你可能还觉得不过瘾。接下来我们看看Java语法糖在字节码里是怎样被“脱糖”的——这是面试最爱考、也最容易用字节码说清的点。先看字符串拼接。用JDK 8编译这段代码public String concat(String a, String b) { return a b; }在JDK 8中字节码会把字符串拼接改成这样关键指令示意invokevirtual StringBuilder.append invokevirtual StringBuilder.append invokevirtual StringBuilder.toString也就是说编译器自动new了一个StringBuilder多次append后toString。这就是为什么很多人强调“循环里别用拼接字符串”——每次循环都会新建一个StringBuilder对象白白产生垃圾。但是注意JDK 9之后javac会改为使用invokedynamic配合StringConcatFactory来做拼接字面上不再直接显示StringBuilder指令。所以如果你在JDK 17上看字符串拼接的字节码看到的会是invokedynamic核心思想已经变了。这种版本差异不亲眼看一下字节码光靠背结论很容易过时。再看lambda表达式。例如Runnable r () - System.out.println(hello);编译后的字节码里不会有一个叫lambda$xxx的普通方法吗会有的。实际是lambda主体被编译成一个私有静态方法比如lambda$main$0然后在main方法里通过invokedynamic指令去调用LambdaMetafactory.metafactory由它运行时生成一个实现了Runnable接口的实例。这个机制让lambda的实例化从“编译期固定”变成了“运行时延迟”也是为什么lambda要捕获外部变量时只能捕获“事实上final”的变量——字节码层面已经把这个约束写死了。自动拆装箱也一样。写Integer x 100; int y x;字节码里会有Integer.valueOf(100)和intValue()的调用。包装类型和基本类型之间的转换在源码里是无感的但JVM里其实发生了一次方法调用。看字节码能让你对这些“免费操作”保持敏感。4.4 用javap -c -p看更多真实细节刚才的例子只有一个方法但实际类里可能有private方法、静态方法、构造器、重载方法javap默认只显示public/protected的方法。想看全部记得加-p。我用一个真实点儿的例子说明。假如类里有个private的静态工具方法public class Demo { private static int secret(int x) { return x * 2; } }不假思索地执行javap -c Demo大概率看不到secret方法。必须执行javap -c -p Demo才能看到它的完整字节码。排查问题或分析框架源码时private方法往往才是核心逻辑所在所以-p这参数我非常推荐默认带上。另外javap -v里还有个容易被忽略的属性叫StackMapTable。在Java 6之后的版本里类加载的字节码验证阶段会读取它来检查类型安全。我们平时不太需要手动阅读它的每个字节但当你自己动手改字节码用ASM或手动编辑class时一旦忘记更新StackMapTableJVM会抛VerifyError——这是新手搞字节码增强最容易撞的墙后面会细说。5. 排查实战——字节码相关的常见问题与解决思路5.1 常见问题速查表我把自己在实际中踩过、帮别人解决过的字节码相关典型问题整理成一张速查表遇到对应报错可以直接按图索骥问题现象可能原因解决思路UnsupportedClassVersionErrorclass文件主版本号高于当前JVM支持版本升级JDK或者用低版本JDK重新编译javap: Class not found类路径没指定或类名写错确认路径用-cp指定jar包或class目录NoClassDefFoundError类加载时某个依赖类缺失检查classpath看常量池里的符号引用能否解析VerifyError字节码校验失败StackMapTable或栈帧不匹配检查改字节码的工具是否更新了StackMapTable反编译代码和预期不一致编译器做了脱糖反编译工具尽力还原用javap -c看原始字节码不要依赖反编译泛型类型在字节码里消失了类型擦除ListString编译后变成List看Signature属性或方法描述符不要指望字节码里保留泛型信息javap显示乱码或中文输出异常终端编码问题Windows下执行chcp 65001切换到UTF-85.2 在字节码里看不到泛型它真的“失踪”了吗有次在群里看到有人问“我用javap -c看一个ListString类型的字段为什么输出里只有java.util.List完全看不到String的痕迹”这就是类型擦除Type Erasure的典型表现。Java泛型是编译期概念编译器在生成字节码的时候把泛型参数信息从方法指令里抹掉了所以运行时无法直接拿到List里具体元素类型。但是JVM又不是完全把泛型信息扔掉——它在class文件的Signature属性里保留了一份“元数据”供反射和编译工具使用。想看这份信息javap -s就能输出方法或字段的“内部签名”。比如字段的签名可能是Ljava/util/ListLjava/lang/String;;指令层面看不到但签名里看得见。这也是为什么Gson、Jackson这类库能借助反射获取泛型类型——它们读的正是Signature属性。我把这个逻辑讲给提问者时他才恍然大悟泛型不是完全消失而是“从执行信息转移到了元数据信息”。5.3 动态代理的字节码小实验热搜词里“java动态代理”常常和“字节码”一起出现这绝对不是偶然。用JDK动态代理举例子你调用Proxy.newProxyInstance(...)时JVM会在运行时动态生成一个名为$Proxy0的class它实现了你传入的接口并把每个接口方法转发到InvocationHandler.invoke。以前我一直觉得这是个“黑盒”直到有一次我用javap看了这个动态生成的类一切才豁然开朗。怎么看到运行时动态生成的代理类的字节码在启动参数加这个JVM参数-Dsun.misc.ProxyGenerator.saveGeneratedFilestrueJDK 8下代理类会保存到当前目录的com/sun/proxy/$Proxy0.class。然后直接javap -c它就能看到类似这样的逻辑public final void com.example.Hello.sayHello(); ...这里先是方法调用的参数准备... ...然后调用 InvocationHandler.invoke 方法...也就是说代理类本身没做任何业务处理它就是按接口定义“照搬”方法签名把所有调用委托给一个InvocationHandler。这套机制的本质就是在字节码层面动态生成一个“转发器”。搞懂了这一层Spring AOP那个“拦截方法”的原理也就不神秘了——AOP框架在运行时要么生成代理类要么用CGLIB/ASM直接生成子类字节码两者都离不开对字节码的理解。5.4 进阶方向从字节码到机器码如果你看完javap -c还不满足想看看JIT编译后真正执行的机器码长什么样那就该请出-XX:PrintAssembly这个JVM参数了。它能配合hsdis插件输出JIT编译后的汇编代码是深入理解JIT优化比如方法内联、锁消除、逃逸分析的利器。但注意这需要额外下载hsdis-amd64.so或.dll插件放到JRE的lib目录下步骤稍繁琐而且输出量极大新手容易淹没在汇编的汪洋里。我的建议是先别急着上汇编级调试。**把字节码学好已经是区分“会用Java”和“懂Java”的重要分水岭了。**机器码是JIT在运行时按CPU架构生成的不同机器、不同参数编译结果都不一样可复现性差而字节码是编译期的静态产物任何时候打开都是同一份。日常排查问题字节码这个层级基本够用了。6. 最后再分享几个实操小技巧前面把查看字节码的完整链路讲完了。最后分享三个我平时使用的小细节都是文档里不太会写、但实际很顺手的东西。第一个技巧javap的-c输出里指令前面的数字不是行号是字节码指令在方法里的偏移量。比如7: if_icmple 20这里的7指的是这条指令在整个方法字节码数组里的起始偏移。调试异常栈时Stack trace里的at Demo.add(Demo.java:10)是行号但JVM内部定位字节码时用的就是偏移量。如果你在做字节码插桩、动态修改逻辑这个偏移量概念一定要刻在脑子里偏移算错一位整个方法就废了。第二个技巧用javap查看源码行号映射关系。当javap -v输出的LineNumberTable里出现“line 10: 0”这样的映射时表示源码第10行对应字节码偏移量0。逻辑异常时根据异常栈的源码行号反查LineNumberTable能快速定位是哪个字节码指令出的问题。对于线上排查偶尔有用属于“冷门但关键时刻救命”的技能。第三个技巧别把字节码学习和面试八股剥离。我在准备面试时筛选过很多关于动态代理、String拼接、lambda原理的题目。与其背别人的结论不如花20分钟把Test类写好分别用JDK 8和JDK 17编译再用javap -c对比差异。自己得出的结论不仅能讲得更具体还能现场演示这种深度是普通背诵替代不了的。说回最开始那个线上性能问题的排查。正是因为我在字节码里看到了循环内字符串拼接产生的StringBuilder对象和不断扩容数组拷贝才定位到了GC压力来源。那次经历让我彻底转变了观念Java源码是给人看的字节码是给JVM看的而一个Java工程师真正的进阶恰恰是从学会阅读中间那层“翻译”开始的。你可以现在就拿手头任意一个编译好的class文件练手javap -c也好IDEA插件也好挑一个自己项目里最简单的类先试试。看懂了第一个后面就是水到渠成的事。
返回列表