深入JVM内核(十)

JAVA 2015-12-23

下面我们来探究JVM字节码的执行

我们知道javap是jdk自带的一个工具,可以反编译,也可以查看java编译器生成的字节码,是分析代码的一个好工具。下面我们就用它对程序进行分析。

看一段代码:

 public class Calc {
    public int calc() {
        int a = 500;
        int b = 200;
        int c = 50;
        return (a + b) / c;
    }
}

使用 javap –verbose Calc 反编译得到:

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
}

注意:-verbose 输出堆栈大小、各方法的 locals 及 args 数,以及class文件的编译版本,其它参数可以输入javap -help查看。

我们按照这个过程逐步分析:

1. 1.png

sipush :500入栈

2. 2.png

3.

3.png

4.

4.png

iload_1 第一个局部变量压栈

5.

5.png iadd 2个数出栈 相加,和 入栈

6.

8.png

idiv 2元素出栈 结果入栈 ireturn 将栈顶的整数返回

注意:字节码指令为一个byte整数

我们来看一个void setAge(int) 方法的字节码:2A 1B B5 00 20 B1

2A 表示_aload_0,没有参数,该操作将局部变量slot0 作为引用 压入操作数栈 1B 表示_iload_1,没有参数,将局部变量slot1 作为整数 压入操作数栈 B5 表示_putfield,设置对象中字段的值,参数为2bytes (00 20) (指明了字段),并弹出栈中2个对象:objectref, value,将栈中的value赋给objectref的给定字段。 B1 表示_return

知道了字节码的执行过程,下面我们看看一些常用的字节码操作

1.常量入栈

  • aconst_null null对象入栈
    • iconst_m1 int常量-1入栈
    • iconst_0 int常量0入栈
    • iconst_5
    • lconst_1 long常量1入栈
    • fconst_1 float 1.0入栈
    • dconst_1 double 1.0 入栈
    • bipush 8位带符号整数入栈
    • sipush 16位带符号整数入栈
    • ldc 常量池中的项入栈

2.局部变量压栈

  • xload(x为i l f d a) 分别表示int,long,float,double,object ref
  • xload_n(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

3.出栈装载入局部变量

  • xstore(x为i l f d a) 出栈,存入局部变量
  • xstore_n(n 0 1 2 3) 出栈,将值存入第n个局部变量
  • xastore(x为i l f d a b c s) 将值存入数组中 iastore 执行前,栈:...,arrayref, index, value 执行后,栈:... 将value存入arrayref[index]

4.通用栈操作(无类型)

  • nop
  • pop 弹出栈顶1个字长
  • dup 复制栈顶1个字长,复制内容压入栈

    5.类型转化

    i2l,i2f,l2i,l2f,l2d,f2i,f2d,d2i,d2l,d2f,i2b,i2c,i2s

    例如:i2l,将int转为long,执行前,栈:..., value,执行后,栈:...,result.word1,result.word2,弹出int,扩展为long,并入栈

    6.整数运算,iadd,ladd,isub,lsub,idiv,ldiv,imul,lmul,iinc;浮点运算:fadd,dadd,fsub,dsub,fdiv,ddiv,fmul,dmul

    7.对象操作指令:new,getfield,putfield,getstatic,putstatic

    8.条件控制:ifeq 如果为0,则跳转;ifne 如果不为0,则跳转;iflt 如果小于0 ,则跳转;ifge 如果大于0,则跳转;if_icmpeq 如果两个int相同,则跳转。 具体来看ifeq,参数为byte1和byte2,value出栈 ,如果栈顶value为0则跳转到(byte1<<8)|byte2,执行前,栈:...,value,执行后,栈:...

    9.方法调用:invokevirtual,invokespecial,invokestatic,invokeinterface,xreturn(x为 i l f d a 或为空)

    下面补充介绍如何使用ASM生成Java字节码。

    ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。AspectJ,Clojure,Ecplise,spring,cglib,hibernate都用到了ASM

    使用ASM框架需要导入asm的jar包,下载地址时ASM下载

    下面看一段helloworld示例:

    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", "<init>", "()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,由于字节码执行性能较差,所以可以对于热点代码编译成机器码再执行,在运行时的编译,叫做JIT Just-In-Time JIT的基本思路是,将热点代码,就是执行比较频繁的代码,编译成机器码。

6.png

当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“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();
        }
    }
}

7.png

craeteStringBuffer: 190 ms

还可以用以下这些参数来运行查看比较

  • Xint 解释执行
  • Xcomp 全部编译执行
  • Xmixed 默认,混合

    至此,我们对JVM的分析介绍结束了,由于本人水平有限,文章难免有所不足,希望各位朋友积极指正。


本文由 Tony 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!