字节码分析与操作

1.1什么是字节码

https://zh.wikipedia.org/wiki/Java%E5%AD%97%E8%8A%82%E7%A0%81

Java所宣称的一次编译处处运行就是靠的字节码技术,java文件编译后会生成字节码文件.class,供jvm使用。字节码文件是由十六进制值组成,两个十六进制为一组,以一个字节为单位进行读取。

编译 javac *.java

反编译javap -c -verbose *.class

1.2.字节码结构

public class ByteCodeDemo {
    private int a = 1;

    public int add() {
        int b = 2;
        int c = a + b;
        System.out.println(c);
        return c;
    }

    public static void main(String[] args) {
        System.out.println("sss");
    }
}

编译后生成的.class文件,这里我们用notepad++ 和 HEX-Editor插件查看这个十六进制文件

分析文件

(1) 魔数(Magic Number)

所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,避免不必要的操作。

  cafeebabe是java之父James Gosling制定的,Java的图标为一杯咖啡,应该是有关系的。

(2) 版本号

版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。

(3) 常量池(Constant Pool)

常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图所示。

常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。示例代码的字节码前10个字节如下图所示,将十六进制的2d转化为十进制值为46,排除掉下标“0”,也就是说,这个类文件中共有46个常量。

(4) 访问标志

常量池结束之后的两个字节,描述该class为类还是接口,以及是否被public,abstract,final等修饰过。JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

(5) 当前类名

访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

(6) 父类名称

当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

(7) 接口信息

父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。

(8) 字段表

字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:

(9)方法表

字段表结束后为方法表,方法表分为两部分,第一部分为用两字节描述方法的个数,第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:

(10)附加属性表

字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

1.3查看字节码的工具

classlib,可以在idea内install这个插件

代码编译后在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息

2字节码操作增强

2.1 ASM

https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html

使用ASM可以直接生产.class文件,在类被加载进jvm之前动态修改。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。主要是利用了访问者设计模式。

2.1.1.1 ASM 核心API

ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

ClassReader:用于读取已经编译好的.class文件。

ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。

2.1.1.2树形API

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

2.1.2 直接利用ASM实现AOP

package asm;

public class Base {
    public void process() {
        System.out.println("process");
    }
}

我们的目的是在process之前和之后都进行操作。

为了利用ASM实现AOP,需要定义两个类:一个是MyClassVisitor类,用于对字节码的visit以及修改;另一个是Generator类,在这个类中定义ClassReader和ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。Generator类较简单,我们先看一下它的实现,如下所示,然后重点解释MyClassVisitor类。

package asm;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class Generator {
    public static void main(String[] args) throws IOException {
        //读取
        ClassReader classReader = new ClassReader("asm/Base");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //处理
        ClassVisitor classVisitor = new MyClassVisitor(classWriter);
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        byte[] data = classWriter.toByteArray();
        //输出
        File f = new File("D:\\program\\java project\\guava\\target\\classes\\asm\\Base.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(data);
        fout.close();
        System.out.println("now generator cc success!!!!!");

        new Base().process();
    }
}

MyClassVisitor继承自ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:

package asm;

import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;

public class MyClassVisitor extends ClassVisitor implements Opcodes {
    public MyClassVisitor(ClassVisitor visitor) {
        super(ASM5, visitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature,
                      String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);
        //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
        if (!name.equals("<init>") && mv != null) {
            mv = new MyMethodVisitor(mv);
        }
        return mv;
    }
}

class MyMethodVisitor extends MethodVisitor implements Opcodes {
    public MyMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("start");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                || opcode == Opcodes.ATHROW) {
            //方法在返回之前,打印"end"
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("end");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        mv.visitInsn(opcode);
    }
}

运行generator之前的Base.class

package asm;

public class Base {
    public Base() {
    }

    public void process() {
        System.out.println("process");
    }
}

运行之后

package asm;

public class Base {
    public Base() {
    }

    public void process() {
        System.out.println("start");
        System.out.println("process");
        System.out.println("end");
    }
}

分析:

首先通过MyClassVisitor类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法 <init> 后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。

接下来,进入内部类MyMethodVisitor中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。

MyMethodVisitor继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。

综上,重写MyMethodVisitor中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

2.1.3 ASM工具

idea install 插件ASM Bytecide Outline

使用方法是对需要操作的java文件右键show bytecode outline,然后在弹出的标签页中选ASMified

直接复制ok

2.2Javassist

强调源代码层次操作字节码的框架Javassist。

利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。

ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。

CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

示例

package asm;

import javassist.*;

import java.io.IOException;

public class JavassistTest {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("asm.Base");
        CtMethod m = cc.getDeclaredMethod("process");
        m.insertBefore("{ System.out.println(\"start\"); }");
        m.insertAfter("{ System.out.println(\"end\"); }");
        Class c = cc.toClass();
        cc.writeFile("D:\\program\\java project\\guava\\target\\classes");
        Base base = (Base) c.newInstance();
        base.process();
    }
}

改造后的class文件

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asm;

public class Base {
    public Base() {
    }

    public void process() {
        System.out.println("start");
        System.out.println("start");
        System.out.println("process");
        Object var2 = null;
        System.out.println("end");
        Object var4 = null;
        System.out.println("end");
    }

    public void test() {
        System.out.println("test");
    }
}

3.4使用场景

热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。

Mock:测试时候对某些服务做Mock。

性能诊断工具:比如bTrace就是利用Instrument,实现无侵入地跟踪一个正在运行的JVM,监控到类和方法级别的状态信息。

参考引用

https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

https://blog.csdn.net/u011810352/article/details/80316870

end

原文地址:https://www.cnblogs.com/CherryTab/p/12210060.html

时间: 2024-10-07 09:46:54

字节码分析与操作的相关文章

通过字节码分析java中的switch语句

在一次做题中遇到了switch的问题,由于对switch执行顺序的不了解,在这里简单的通过字节码的方式理解一下switch执行顺序(题目如下): public class Ag{ static public int i=10; public static void main(String []args){ switch(i){ default: System.out.println("this is default"); case 1: System.out.println("

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 package com.roocon.thread.t3; public class Sequence { private int value; public int getNext(){ return value++; } public static void main(String[] args) { S

JVM-String比较-字节码分析

一道String字符串比较问题引发的字节码分析 public class a { public static void main(String[] args)throws Exception{ } public static void aa(){ String s1="a";//内存在方法区的常量池 String s2="b";//内存在方法区的常量池 String s12 = "ab";//内存在方法区的常量池 String s3 = s1 +

java中i=i++字节码分析

原文出处: Ticmy 1 2 int i = 0; i = i++; 结果还是0为什么? 程序的执行顺序是这样的:因为++在后面,所以先使用i,"使用"的含义就是i++这个表达式的值是0,但是并没有做赋值操作,它在整个语句的最后才做赋值,也就是说在做了++操作后再赋值的,所以最终结果还是0 让我们看的更清晰点: 1 2 int i = 0;//这个没什么说的 i = i++;//等效于下面的语句: 1 2 3 int temp = i;//这个temp就是i++这个表达式的值 i++

字节码分析finally块对return返回值的影响

直接进入主题.看如下代码: public int test(){ int i=0; try { i=1; return i; } catch (Exception e) { i=2; return i; }finally{ i=3; } } 相信有点经验的程序员一眼就能说出返回的结果为1,但是您真的知道返回的结果为什么为1吗?下面我们通过分析下当前方法的字节码,来说明为什么. 查看字节码命令:javap -verbose class文件 ? 知识点简单概要:看如下字节码需要简单了解下栈的结构.栈

通过字节码分析JDK8中Lambda表达式编译及执行机制

关于Lambda字节码相关的文章,很早之前就想写了,[蜂潮运动]APP 产品的后端技术,能快速迭代,除了得益于整体微服架构之外,语言层面上,也是通过Java8的lambda表达式的运用以及rxJava响应式编程框架,使代码更加简洁易维护,调用方式更加便捷.本文将介绍JVM中的方法调用相关的字节码指令,重点解析JDK7(JSR-292)之后新增的invokedynamic指令给lambda表达式的动态调用特性提供的实现机制,最后再探讨一下lambda性能方面的话题. 方法调用的字节码指令 在介绍i

用字节码分析Java的For循环

Fou循环常常用,但是在字节码层它是怎样执行的呢?出于兴趣驱使,就有了这篇短文了! 首先要分析字节码就得先写个类了,代码如下: public class ForTest{ public static void main(String[] args) { for (int i = 0; i < 10; ++i) { System.out.println(i); } } } 简单的for循环打印10个数,那么编译之后就该得到他的字节码了,使用命令 javap -v ForTest.class > 

深挖JDK动态代理(二):JDK动态生成后的字节码分析

接上一篇文章深挖JDK动态代理(一)我们来分析一下JDK生成动态的代理类究竟是个什么东西 1. 将生成的代理类编程一个class文件,通过以下方法 public static void transClass() throws IOException { URL resource = rpcMain.class.getClass().getResource("/"); byte[] bts = ProxyGenerator.generateProxyClass("$Proxy0

透过字节码分析java基本类型数组的内存分配方式。

我们知道java中new方式创建的对象都是在堆中创建的,而局部变量对应的值存放在栈上.那么java中的int [] arr={1,2,3}是存放在什么地方的呢,int []arr = new int[3]又是存放在什么地方的呢, 下面我们通过编写两个小例子,通过查看生成的字节码文件,来了解jvm会如何来处理这两种情况的. 1.int[] arr = new int[3]示例 public class ArrayTest { public static void main(String[] arg