深入学习JVM探针与字节码技术

JVM探针是自jdk1.5以来,由虚拟机提供的一套监控类加载器和符合虚拟机规范的代理接口,结合字节码指令能够让开发者实现无侵入的监控功能。如:监控生产环境中的函数调用情况或动态增加日志输出等等。虽然在常规的业务中不会有太多用武之地,但是作为一项高级的技术手段也应该是资深开发人员的必备技能之一。同时,它也是企业级开发和生产环境部署不可或缺的技术方案,是对当下流行的APM的一种补充,因为使用探针技术能够实现比常规APM平台更细粒度的监控。

哪些方面适合使用探针技术:

  • (1) 如果你发现生产环境上有些问题无法在测试或开发环境中复现

  • (2) 如果你希望在不修改源码的情况下为你的应用添加一些输出日志

  • (3) 如果在刚发布的生产包中发现了一个bug,而你又不希望被它阻断,希望有一个临时的补救措施

一、JVM探针:Instrumentation

使用探针只需要一条附加选项:-javaagent:<jar 路径>[=<选项>],作为探针(代理)的jar包必须满足两个条件:1. MANIFEST.MF文件需要增加Premain-Class项,说明启动类。2. 启动类必须声明一个静态函数,它的入参是: String和Instrumentation。因此,一个常见的启动类可能像这样:

package aa.bb.cc;

public class PremainAgent {
   
    public static void premain(String agentArgs, Instrumentation inst) {
   
        // TODO
    }
}

MANIFEST.MF

premain-class: aa.bb.cc.PremainAgent

如果使用maven作为构建工具,需要在pom文件中添加构建插件

<plugin>  
    <groupId>org.apache.maven.plugins</groupId>  
    <artifactId>maven-jar-plugin</artifactId>  
    <version>3.2.0</version>  
    <configuration>  
        <archive>  
            <manifestEntries>  
                <premain-class>aa.bb.cc.PremainAgent</premain-class>  
            </manifestEntries>  
        </archive>  
    </configuration>  
</plugin>

如果你还引入了其它依赖希望同时打包,那么你应该使用assembly插件替代

<plugin>  
  <groupId>org.apache.maven.plugins</groupId>  
  <artifactId>maven-assembly-plugin</artifactId>  
  <version>2.4</version>  
  <configuration>  
    <descriptorRefs>  
      <descriptorRef>jar-with-dependencies</descriptorRef>  
    </descriptorRefs>  
    <archive>  
      <manifestEntries>  
        <Premain-Class><package>.PremainAgent</Premain-Class>
        <Can-Redefine-Classes>true</Can-Redefine-Classes>  
        <Can-Retransform-Classes>true</Can-Retransform-Classes>  
      </manifestEntries>  
    </archive>  
  </configuration>  
  <executions>  
    <execution>  
      <phase>package</phase>  
      <goals>  
        <goal>single</goal>  
      </goals>  
    </execution>  
  </executions></plugin>

两个重要的类

Instrumentation: 由JDK提供的一个探针类,它会负责加载用户自定义的ClassFileTransformer

ClassFileTransformer: 字节码转换类,jvm在加载class文件前会先调用它,对所有类加载器有效

具体用法稍后会做详细介绍。

总结:JVM探针只是提供了一种让开发人员能够在类加载加载class文件前主动介入的一种方法,具体如何操作需要开发人员了解Java虚拟机规范以及字节码的相关知识。

二、栈帧与指令集

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。


局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。局部变量表类似一个数组结构,虚拟机在访问局部变量表的时候会使用下标作为引用,普通方法的局部变量表中第0位索引默认是用于传递方法所属对象实例的引用this。

操作数栈(Operand Stack)和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。

动态链接(Dynamic Linking)每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支付方法调用过程中的动态连接。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用,这部分称为动态连接。

返回地址:当一个方法开始执行后,只有2种方式可以退出这个方法,方法返回指令和异常退出。无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

JVM指令集并非是对Java语句的直接翻译,由于指令只使用1个字节表示,所以指令集最多只能包含256种指令。因此,一条Java语句一般会对应多条底层指令。每一条指令都有与之对应的助记符,我们可以通过官方资料查看它们对应关系:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html。为了帮助大家更加直观的理解字节码指令,我将通过三个用例分别解释。

从一个简单的加法函数开始,我们可以使用javac将.java文件编译成.class,再通过javap -c查看它的字节码文件

public int add(int x, int y) {
     
    return x + y;  
}
public add(II)I
    ILOAD 1 // 将局部变量表中#1变量入栈
    ILOAD 2 // 将局部变量表中#2变量入栈
    IADD // 调用整型数相加(两个数出栈,再将结果入栈)
    IRETURN 
  • 0
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值