此博客为炼数成金JVM教程第十一课
目录
javap
class 文件反编译工具
有如下代码:
public class Calc{
public int calc(){
int a = 500;
int b = 200;
int c = 50;
return (a+b)/c;
}
}
用javap 反编译
javac Calc.java
javap -verbose Calc
结果:
Classfile /C:/Users/Administrator/Desktop/code/Calc.class
Last modified 2018-6-15; size 262 bytes
MD5 checksum 47226419c37bb8f7e29890883f8a5807
Compiled from "Calc.java"
public class Calc
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."":()V
#2 = Class #13 // Calc
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 calc
#9 = Utf8 ()I
#10 = Utf8 SourceFile
#11 = Utf8 Calc.java
#12 = NameAndType #4:#5 // "":()V
#13 = Utf8 Calc
#14 = Utf8 java/lang/Object
{
public Calc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public int calc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: sipush 500
3: istore_1
4: sipush 200
7: istore_2
8: bipush 50
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: idiv
16: ireturn
LineNumberTable:
line 4: 0
line 5: 4
line 6: 8
line 7: 11
}
SourceFile: "Calc.java"
具体我们看
public int calc();
Code:
stack=2, locals=4, args_size=1
0: sipush 500
3: istore_1
4: sipush 200
7: istore_2
8: bipush 50
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: idiv
16: ireturn
}
其中 1 3 4 … 16,对于字节码来说是偏移量,广义上来说可以理解为行号
这些行代表的是:方法运行时需要运行的指令
上述字节码执行过程
我们需要关注:
程序计数器:每个线程都有一个单独的计数器指向当前正在执行的 指令的位置
局部变量表
操作数栈
执行 sipush
该方法为实例方法,this作为第一个参数放在局部变量表,
sipush 把500 放入到操作数栈
执行istore_1
从操作数栈弹出一个数字到局部变量表
操作数栈被清空,局部变量表存储500
执行sipush 200
把200 压入操作数栈
执行bipush 50
把50 压入操作数栈
执行iload_1
把局部变量表的第一个位置的整数数据压入到操作数栈
执行iload_2
把局部变量表的第二个位置的整数数据压入到操作数栈
执行iadd
将两个数从操作数栈中弹出,做相加操作,再将相加的值压入操作数栈
执行iload_3
将局部变量表第三个位置的整数数据压入到操作数栈
执行idiv
从操作数栈中弹出两个数,做除法,将结果压入操作数栈
执行ireturn
方法体的返回,返回值为操作数栈的栈顶的元素
字节码指令为一个byte整数
左半部分:注解符,帮助理解
右半部分:在程序中真正的表示方式
_nop = 0, // 0x00
_aconst_null = 1, // 0x01
_iconst_0 = 3, // 0x03
_iconst_1 = 4, // 0x04
_dconst_1 = 15, // 0x0f
_bipush = 16, // 0x10
_iload_0 = 26, // 0x1a
_iload_1 = 27, // 0x1b
_aload_0 = 42, // 0x2a
_istore = 54, // 0x36
_pop = 87, // 0x57
_imul = 104, // 0x68
_idiv = 108, // 0x6c
void serAge(int) 方法的字节码:
2A 1B B5 00 20 B1
2A _aload_0
无参
将局部变量slot0 作为引用 压入操作数栈
1B _iload_1
无参
将局部变量slot1 作为整数 压入操作数栈
B5 _putfield
设置对象中字段的值
参数为 2bytes (00 20) (指明了字段)
指向常量池 的引用
Constant_Fieldref
此处为User.age
弹出栈中2个对象:objectref, value
将栈中的value赋给objectref的给定字段
B1 _return
常量入栈
aconst_null null对象入栈
iconst_m1 int常量-1入栈
iconst_0 int常量0入栈
iconst_5 int常量5入栈
lconst_1 long常量1入栈
fconst_1 float 1.0入栈
dconst_1 double 1.0 入栈
bipush 8位带符号整数入栈
sipush 16位带符号整数入栈
ldc 常量池中的项入栈
局部变量压栈
xload(x为i l f d a)
分别表示int,long,float,double,object ref
xload_n(x为i l f d a,n为0 1 2 3)
表示第几个变量入栈
xaload(x为i l f d a b c s)
分别表示int, long, float, double, obj ref ,byte,char,short
从数组中取得给定索引的值,将该值压栈
iaload
执行前,栈:…, arrayref, index
它取得arrayref所在数组的index的值,并将值压栈
执行后,栈:…, value
出栈装载入局部变量
xstore(x为i l f d a)
出栈,存入局部变量
xstore_n(x为i l f d a, n 0 1 2 3)
出栈,将值存入第n个局部变量
xastore(x为i l f d a b c s)
将值存入数组中
iastore
执行前,栈:…,arrayref, index, value
执行后,栈(被清空):…
将value存入arrayref[index]
通用栈操作(无类型)
nop 什么都不做
pop
弹出栈顶1个字长
dup
复制栈顶1个字长,复制内容压入栈
类型转换
例子:i2l
将int转为long
执行前,栈:…, value
执行后,栈:…,result.word1,result.word2
弹出int,扩展为long,并入栈
整数运算
浮点运算
对象操作指令
new 生成对象
getfield 从对象中拿出给定的值
putfield 从对象中设置给定的值
getstatic 针对静态的字段拿值
putstatic 针对静态的字段设置值
条件控制
ifeq 如果为0,则跳转
ifne 如果不为0,则跳转
iflt 如果小于0 ,则跳转
ifge 如果大于0,则跳转
if_icmpeq 如果两个int相同,则跳转
例子:ifeq
参数 byte1,byte2
value出栈 ,如果栈顶value为0则跳转到(byte1<<8)|byte2
执行前,栈:…,value
执行后,栈:…
方法调用
invokevirtual: 对一个普通实例的类方法进行调用(动态绑定)
invokespecial: 对父类的方法进行调用(静态绑定)
invokestatic: 对静态的方法进行调用
invokeinterface: 对接口方法进行调用
xreturn(x为 i l f d a 或为空): 方法的返回
ASM
jva字节码操作框架
可以用于修改现有类或者动态产生新类
用户:
AspectJ
Clojure
Eclipse
spring
cglib
hibernate
代码实现例子:
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_7, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "" , "()V", null, null);
mw.visitVarInsn(ALOAD, 0); //this 入栈
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()V");
mw.visitInsn(RETURN);
mw.visitMaxs(0, 0);
mw.visitEnd();
mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mw.visitLdcInsn("Hello world!");
mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mw.visitInsn(RETURN);
mw.visitMaxs(0,0);
mw.visitEnd();
byte[] code = cw.toByteArray();
AsmHelloWorld loader = new AsmHelloWorld();
Class exampleClass = loader
.defineClass("Example", code, 0, code.length);
exampleClass.getMethods()[0].invoke(null, new Object[] { null });
模拟实现AOP字节码织入
在函数开始部分或结束部分嵌入字节码
可用于进行鉴权,日志等
public class Account {
public void operation() {
System.out.println("operation....");
}
}
在这个方法执行前鉴权或者添加日志
我们要嵌入的内容
public class SecurityChecker {
public static boolean checkSecurity() {
System.out.println("SecurityChecker.checkSecurity ...");
return true;
}
}
ASM 实现
class AddSecurityCheckClassAdapter extends ClassVisitor {
public AddSecurityCheckClassAdapter( ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
// 重写 visitMethod,访问到 "operation" 方法时,
// 给出自定义 MethodVisitor,实际改写方法内容
public MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature, final String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
MethodVisitor wrappedMv = mv;
if (mv != null) {
// 对于 "operation" 方法
if (name.equals("operation")) {
// 使用自定义 MethodVisitor,实际改写方法内容
wrappedMv = new AddSecurityCheckMethodAdapter(mv);
}
}
return wrappedMv;
}
}
class AddSecurityCheckMethodAdapter extends MethodVisitor {
public AddSecurityCheckMethodAdapter(MethodVisitor mv) {
super(Opcodes.ASM5,mv);
}
public void visitCode() {
visitMethodInsn(Opcodes.INVOKESTATIC, "geym/jvm/ch10/asm/SecurityChecker",
"checkSecurity", "()Z");
super.visitCode();
}
}
public class Generator{
public static void main(String args[]) throws Exception {
ClassReader cr = new ClassReader("geym.jvm.ch10.asm.Account");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES);
AddSecurityCheckClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
cr.accept(classAdapter, ClassReader.SKIP_DEBUG);
byte[] data = cw.toByteArray();
File file = new File("bin/geym/jvm/ch10/asm/Account.class");
FileOutputStream fout = new FileOutputStream(file);
fout.write(data);
fout.close();
}
}
输出结果
SecurityChecker.checkSecurity ...
operation....
字节码执行性能较差,所以可以对于热点代码编译成机器码再执行,在运行时的编译,叫做JIT Just-In-Time
JIT的基本思想是,将热点代码,就是执行比较频繁的代码,编译成机器码
JIT
当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为”Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码
-XX:CompileThreshold=1000, 调用多少次的阈值设置
-XX:+PrintCompilation: 打出方法编译的信息
public class JITTest {
public static void met(){
int a=0,b=0;
b=a+b;
}
public static void main(String[] args) {
for(int i=0;i<1000;i++){
met();
}
}
}
运行参数
-XX:CompileThreshold=1000 -XX:+PrintCompilation
运行输出:
56 1 java.lang.String::hashCode (55 bytes)
56 2 java.lang.String::equals (81 bytes)
57 3 java.lang.String::indexOf (70 bytes)
60 4 java.lang.String::charAt (29 bytes)
61 5 java.lang.String::length (6 bytes)
61 6 java.lang.String::lastIndexOf (52 bytes)
61 7 java.lang.String::toLowerCase (472 bytes)
67 8 geym.jvm.ch2.jit.JITTest::met (9 bytes)
可见,met方法被编译了
JIT的其余三个参数
-Xint
解释执行(不做编译,执行性能比较差)
-Xcomp
全部编译执行(不管方法被调用的次数,所以方法都会被编译成机器码,在启动时性能会非常糟糕,在编异完成后,性能就会好很多)
-Xmixed
默认,混合(解释执行和编译执行,取决于方法被调用的次数)