此部落格為煉數成金JVM教程第十一課
目錄
- javap
- 簡單的位元組碼執行過程
- 常用的位元組碼
- 使用ASM生成java位元組碼
- JIT及其相關參數
javap
class 檔案反編譯工具
有如下代碼:
public class Calc{
public int calc(){
int a = ;
int b = ;
int c = ;
return (a+b)/c;
}
}
用javap 反編譯
javac Calc.java
javap -verbose Calc
結果:
Classfile /C:/Users/Administrator/Desktop/code/Calc.class
Last modified --; size bytes
MD5 checksum c37bb8f7e29890883f8a5807
Compiled from "Calc.java"
public class Calc
minor version:
major version:
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()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 // "<init>":()V
#13 = Utf8 Calc
#14 = Utf8 java/lang/Object
{
public Calc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=, locals=, args_size=
: aload_0
: invokespecial #1 // Method java/lang/Object."<init>":()V
: return
LineNumberTable:
line :
public int calc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=, locals=, args_size=
: sipush
: istore_1
: sipush
: istore_2
: bipush
: istore_3
: iload_1
: iload_2
: iadd
: iload_3
: idiv
: ireturn
LineNumberTable:
line :
line :
line :
line :
}
SourceFile: "Calc.java"
具體我們看
public int calc();
Code:
stack=, locals=, args_size=
: sipush
: istore_1
: sipush
: istore_2
: bipush
: istore_3
: iload_1
: iload_2
: iadd
: iload_3
: idiv
: ireturn
}
其中 1 3 4 … 16,對于位元組碼來說是偏移量,廣義上來說可以了解為行号
這些行代表的是:方法運作時需要運作的指令
上述位元組碼執行過程
我們需要關注:
程式計數器:每個線程都有一個單獨的計數器指向目前正在執行的 指令的位置
局部變量表
操作數棧
執行 sipush
該方法為執行個體方法,this作為第一個參數放在局部變量表,
sipush 把500 放入到操作數棧
執行istore_1
從操作數棧彈出一個數字到局部變量表
操作數棧被清空,局部變量表存儲500
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX9cGRNFTQ61UNrRVTr5kMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TNxgTM0cTNwETNxYDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
執行sipush 200
把200 壓入操作數棧
istore_2
把200放入局部變量表,操作數棧被清空
執行bipush 50
把50 壓入操作數棧
istore_3
把50放入局部變量表,操作數棧被清空
執行iload_1
把局部變量表的第一個位置的整數資料壓入到操作數棧
執行iload_2
把局部變量表的第二個位置的整數資料壓入到操作數棧
執行iadd
将兩個數從操作數棧中彈出,做相加操作,再将相加的值壓入操作數棧
執行iload_3
将局部變量表第三個位置的整數資料壓入到操作數棧
執行idiv
從操作數棧中彈出兩個數,做除法,将結果壓入操作數棧
執行ireturn
方法體的傳回,傳回值為操作數棧的棧頂的元素
位元組碼指令為一個byte整數
左半部分:注解符,幫助了解
右半部分:在程式中真正的表示方式
_nop = , // 0x00
_aconst_null = , // 0x01
_iconst_0 = , // 0x03
_iconst_1 = , // 0x04
_dconst_1 = , // 0x0f
_bipush = , // 0x10
_iload_0 = , // 0x1a
_iload_1 = , // 0x1b
_aload_0 = , // 0x2a
_istore = , // 0x36
_pop = , // 0x57
_imul = , // 0x68
_idiv = , // 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
- i2f
- l2i
- l2f
- l2d
- f2i
- f2d
- d2i
- d2l
- d2f
- i2b
- i2c
- i2s
例子:i2l
将int轉為long
執行前,棧:…, value
執行後,棧:…,result.word1,result.word2
彈出int,擴充為long,并入棧
整數運算
- iadd
- ladd
- isub
- lsub
- idiv
- ldiv
- imul
- lmul
- iinc(++)
浮點運算
- fadd
- dadd
- fsub
- dsub
- fdiv
- ddiv
- fmul
- dmul
對象操作指令
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生成Java位元組碼
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, "<init>", "()V", null, null);
mw.visitVarInsn(ALOAD, ); //this 入棧
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mw.visitInsn(RETURN);
mw.visitMaxs(, );
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(,);
mw.visitEnd();
byte[] code = cw.toByteArray();
AsmHelloWorld loader = new AsmHelloWorld();
Class exampleClass = loader
.defineClass("Example", code, , code.length);
exampleClass.getMethods()[].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及其相關參數
位元組碼執行性能較差,是以可以對于熱點代碼編譯成機器碼再執行,在運作時的編譯,叫做JIT Just-In-Time
JIT的基本思想是,将熱點代碼,就是執行比較頻繁的代碼,編譯成機器碼
JIT
當虛拟機發現某個方法或代碼塊運作特别頻繁時,就會把這些代碼認定為”Hot Spot Code”(熱點代碼),為了提高熱點代碼的執行效率,在運作時,虛拟機會把這些代碼編譯成與本地平台相關的機器碼
-XX:CompileThreshold=1000, 調用多少次的門檻值設定
-XX:+PrintCompilation: 打出方法編譯的資訊
public class JITTest {
public static void met(){
int a=,b=;
b=a+b;
}
public static void main(String[] args) {
for(int i=;i<;i++){
met();
}
}
}
運作參數
運作輸出:
java.lang.String::hashCode ( bytes)
java.lang.String::equals ( bytes)
java.lang.String::indexOf ( bytes)
java.lang.String::charAt ( bytes)
java.lang.String::length ( bytes)
java.lang.String::lastIndexOf ( bytes)
java.lang.String::toLowerCase ( bytes)
geym.jvm.ch2.jit.JITTest::met ( bytes)
可見,met方法被編譯了
JIT的其餘三個參數
-Xint
解釋執行(不做編譯,執行性能比較差)
-Xcomp
全部編譯執行(不管方法被調用的次數,是以方法都會被編譯成機器碼,在啟動時性能會非常糟糕,在編異完成後,性能就會好很多)
-Xmixed
預設,混合(解釋執行和編譯執行,取決于方法被調用的次數)