E:\jdk8u261\bin\java -cp E:\jdk8u261\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
类数据查看再打开的面板中做如下操作:选择 Attach To HotSpot Process,tools → Class Browser → 输入:com.qunar.sa.TargetProcessStudent→点击:classcom.qunar.sa.TargetProcessStudent @0x00000007c0060a18,类视图如下:
对象数据查看在打开的面板中做如下操作:选择 Attach To HotSpot Process,tools → Find Object By Query → 输入:com.qunar.sa.TargetProcess$Student → 查询 → 点击 Execute,可获得对象视图如下:
可视化工具方式不是本文的重点,另外这块网上资料很多,这里只做简要介绍。
7. JIT 即时编译机制
背景部分说到过我们通过 SA 获取 JVM 中的方法调用数据,从而判断代码是否可下线。有两个问题需要搞清楚:
(1)JVM 为什么会统计方法调用信息?
(2)统计的调用信息准确吗?
第一个问题:JVM 为什么会统计方法调用信息?应该很容易猜到,JVM 使用方法调用统计,将部分高频执行的代码编译为本地机器码,实现 JIT 编译,而想搞清楚第二个问题却没有那么容易,必须深入分析 JIT 执行机制才能知其所以然。
7.1. 方法调用信息存储
方法调用信息存储与上文说的 oop/klass 二分模型中的 Klass 息息相关,Klass 中包含了所有类相关信息,而类中包含方法,所以自然也包含了方法调用统计信息,涉及的源码文件、类及变量如下,感兴趣可以直接查看源码。
7.2. 热编译触发
JVM 为了提高程序执行效率,会在运行时动态执行热编译,将部分高频执行的代码编译为本地机器码。触发方式有以下两类:
(1)方法调用计数器触发:如果方法调用统计达到编译阈值,则对该方法执行编译,这种编译是一个异步的过程,它允许程序在代码正在编译时继续通过解释方式执行,编译完成后将生成的机器码存入 code cache,后面的调用可直接使用。这种编译方式叫做标准编译。
(2)回边计数器触发:在字节码中遇到控制流向后跳转的字节码指令称为”回边“,如 for、while 等。一个方法内回边操作发生时,回边计数器都会自增和自检。回边计数器计数超出其自身阈值时,当前方法获得被编译资格。编译发生时 JVM 仍然通过解释方式执行循环体,编译完成后,下一次循环迭代则会执行新编译的代码。这种编译方式叫做 OSR(栈上替换)。
7.3. 热度衰减
现在我们来探讨下本节开始的第二个问题:jvm 统计的方法调用次数准确吗?我们先给出结论:方法调用计数器统计的并不是方法被调用的绝对次数。
当超过一定的时间限度,如果方法的调用次数仍然不足以触发编译,该方法的调用计数就会被减少一半,这个过程称为方法调用计数器热度衰减,而这段时间就称为此方法统计的半衰周期。那么问题来了,既然方法调用计数器统计的并非方法调用的绝对次数,我们还能不能通过判断调用次数为0,从而做出该方法可下线的判断呢?源码面前无秘密。
以下代码摘自:invocationCounter.hpp
代码片段1:
private:
unsigned int _counter;
enum State {
wait_for_nothing,
wait_for_compile,
number_of_states
代码片段2:
inline void InvocationCounter::decay() {
int c = count();
int new_count = c >> 1;
if (c > 0 && new_count == 0) new_count = 1;
set(state(), new_count);
代码片段1解析:
每一个方法都会对应一个方法调用计数器,为了尽可能节省内存空间,JVM 中将 counter、carry、state 三个字段用一个32位 int 类型数据表示,如代码所示,其中:
counter:第3-31位表示方法调用计数,每次方法调用+1,超过半衰周期未触发编译则数据减半,触发编译后调用 reset 方法重置 InvocationCounter 所有数据。
carry:第2位表示当前方法是否已被编译,方法调用计数器达到阈值并触发编译动作后,carry 被设置为1,表示该方法已被编译。
state:第0位和第1位表示超出阈值时的处理,枚举值如源码中 enum State,waitfornothing 表示方法调用次数超过阈值后不触发编译,waitforcompile 表示方法调用次数超过阈值后触发编译。
代码片段2解析:
通过将调用次数做移位运算,实现热度减半,减半之后的数据如等于0则将调用次数设置为1,以此来区分从未执行的方法。
再回头看下我们要解决的问题,虽然调用次数并不准确,但已执行过的方法不可能为0,通过这种方式可以达到判断代码是否可下线的目的。
7.4. 回边计数器统计原理
下面通过一段代码,分析回边计数器如何统计,并以此讲清楚 jvm 执行机制。代码如下:
public class TestSum{
public static void main(String[] args) {
int sum = 0;
for(int i=0 ; i< 100; i++){
sum += i;
java 代码-TestSum.java:
先执行 javac TestSum.java 将源码转换为字节码,然后执行 javap -v -l TestSum.class 查看生成的字节码内容,结果如下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
将常量0放到栈顶
1: istore_1 将栈顶的int变量放到本地变量表中索引为1的位置,即将0赋值给sum
2: iconst_0 将常量0放到栈顶
3: istore_2 将栈顶的int变量放到本地变量表中索引为2的位置,即将0赋值给i
4: iload_2 将本地变量表中索引为2的变量放到栈顶,即将i=1放入栈顶
5: bipush 100 将byte类型100放到栈顶
7: if_icmpge 20 会比较栈顶的两个值,如果变量i>=100时会跳转到偏移量是20的指令处,否则继续执行下一条字节码
10: iload_1 将本地变量表中索引为1的变量放到栈顶,即将sum放入栈顶
11: iload_2 将本地变量表中索引为2的变量放到栈顶,即将i放入栈顶
12: iadd 将栈顶两个元素求和,将结果放入栈顶,即sum+i放入栈顶
13: istore_1 将栈顶的int变量放到本地变量表中索引为1的位置,即将sum+i赋值给sum
14: iinc 2, 1 将本地变量中索引为2的变量自增1,即变量i的自增
17: goto 4 跳转到偏移量是4的指令处
20: return 方法返回
从上述字节码分析可知,for 循环是通过 goto 指令实现跳转的,下面通过 bytecodeInterpreter 字节码解释器来分析 goto 字节码指令如何实现。
这里解释下 bytecodeInterpreter 字节码解释器,hotspot 实现了两种解释器,即:模板解释器(TemplateInterpreter)和 C++字节码解释器(CppInterpreter、BytecodeInterpreter),其中模板解释器(TemplateInterpreter)是 hotspot 使用的默认解释器,使用汇编语言实现,而 C++字节码解释器使用 C++语言实现,后者为我们阅读源码提供了极大的便利,简单的说 TemplateInterpreter 是将 BytecodeInterpreter 字节码的执行语句从 c/c++代码换成汇编代码而来的。相较模板解释器,字节码解释器的可读性更好。所以我选择通过后者讲清楚回边计数器如何做计数统计。
以下代码摘自:bytecodeInterpreter.cpp
代码片段1:
CASE(_goto):
int16_t offset = (int16_t)Bytes::get_Java_u2(pc + 1);
address branch_pc = pc;
UPDATE_PC(offset);
DO_BACKEDGE_CHECKS(offset, branch_pc);
CONTINUE;
代码片段2:
#define DO_BACKEDGE_CHECKS(skip, branch_pc) \
... \
mcs->backedge_counter()->increment(); \回边统计数+1
if (do_OSR) do_OSR = mcs->backedge_counter()->reached_InvocationLimit(); \判断是否到达回边技术编译阈值
if (do_OSR) { \
nmethod* osr_nmethod; \
OSR_REQUEST(osr_nmethod, branch_pc); \达到阈值则执行提交OSR编译请求
if (osr_nmethod != NULL && osr_nmethod->osr_entry_bci() != InvalidOSREntryBci) { \
intptr_t* buf = SharedRuntime::OSR_migration_begin(THREAD); \
istate->set_msg(do_osr); \
istate->set_osr_buf((address)buf); \
istate->set_osr_entry(osr_nmethod->osr_entry()); \
return; \
} \
} \
... \
7.5. 基于 SA 窥探 JIT 相关数据(代码方式)
我们来看下通过写代码的方式,如何借助 SA 机制观测 JIT 相关数据,包括调用信息及已被 JIT 编译的方法信息,代码及注释如下(篇幅问题,代码不规范,目的是说明核心逻辑):
目标进程代码:
package com.qunar.sa;
* Date:Create at 2020/12/16 17:36
* Description:目标进程代码,目的是观察方法调用情况
* 主要逻辑:
* 1)创建一个Student对象,并调用setId设置id值
* 2)对测试的三个方法执行100、1000、10000次的调用
* @author zhichao.pan
public class TargetProcess {
public static void main(String[] args) throws Exception{
int id = 1;
Student student = new Student();
student.setId(id);
for (int i = 0; i < 100; i++){
student.JITTest1();
for (int i = 0; i < 1000; i++){
student.JITTest2();
for (int i = 0; i < 100000; i++){
student.JITTest3();
Thread.sleep(10000000 * 1000);
static class Student{
private static int type = 10;
private int id;
public void setId(int id) {
this.id = id;
void JITTest1(){
System.out.println(this.id);
void JITTest2(){
System.out.println(this.id);
void JITTest3(){
System.out.println(this.id);
SA 进程代码:
package com.qunar.sa;
......省略import部分代码
* Date:Create at 2020/12/16 17:36
* Description:目标进程代码,目的是观察对象内存分布
* 主要逻辑:
* 1)创建一个Student对象,并调用setId设置id值
* 2)对测试的三个方法执行100、1000、10000次的调用
* @author zhichao.pan
public class SAProcess {
public static void main(String[] args) throws ParseException {
int pid = 20408 ;
HotSpotAgent agent = new HotSpotAgent();
agent.attach(pid);
try {
final Set<MethodDefinition> methodResult = new HashSet<>();
VM.getVM().getSystemDictionary().allClassesDo(new InvocationCounterVisitor(methodResult));
System.out.println("SA遍历方法执行信息:" + JacksonSupport.toJson(methodResult));
final Set<MethodDefinition> compiledMethodResult = new HashSet<>();
VM.getVM().getCodeCache().iterate(new CompiledMethodVisitor(compiledMethodResult));
System.out.println("SA遍历热编译数据:" + JacksonSupport.toJson(compiledMethodResult));
} finally {
agent.detach();
static class InvocationCounterVisitor implements SystemDictionary.ClassVisitor {
private final Set<MethodDefinition> result;
public InvocationCounterVisitor(Set<MethodDefinition> result) {
this.result = result;
@Override
public void visit(Klass klass) {
final String klassName = klass.getName().asString();
if (klassName.contains("Student")) {
final MethodArray methods = ((InstanceKlass) klass).getMethods();
for (int i = 0; i < methods.length(); i++) {
final Method method = methods.at(i);
long invocationCount = method.getInvocationCount();
invocationCount = invocationCount >> 3;
result.add(
new MethodDefinition(klassName, method.getName().asString(),
method.getSignature().asString(),
invocationCount));
static class CompiledMethodVisitor implements CodeCacheVisitor {
private final Set<MethodDefinition> result;
@Override
public void visit(CodeBlob codeBlob) {
final NMethod nMethod = codeBlob.asNMethodOrNull();
if(nMethod == null ) return;
final Method method = nMethod.getMethod();
final String className = method.getMethodHolder().getName().asString();
final String name = method.getName().asString();
final String signature = method.getSignature().asString();
long invocationCount = method.getInvocationCount();
invocationCount = invocationCount >> 3;
if(className.contains("Student")){
result.add(new MethodDefinition(StringUtils.replace(className, "/", "."), name,
SignatureUtils.convertToSourceType(signature), invocationCount));
@Override
public void epilogue() {
public CompiledMethodVisitor(Set<MethodDefinition> result) {
this.result = result;
@Override
public void prologue(Address address, Address address1) {
static class MethodDefinition
{
public String className;
public String methodName;
public String parameters;
public long invocationCount;
public MethodDefinition(String className, String methodName, String parameters, long invocationCount) {
this.className = className;
this.methodName = methodName;
this.parameters = parameters;
this.invocationCount = invocationCount;
运行结果如下:
SA遍历方法执行信息:[
"className":"com/qunar/sa/TargetProcess$Student",
"methodName":"<init>",
"parameters":"()V",
"invocationCount":1
"className":"com/qunar/sa/TargetProcess$Student",
"methodName":"JITTest1",
"parameters":"()V",
"invocationCount":100
"className":"com/qunar/sa/TargetProcess$Student",
"methodName":"JITTest3",
"parameters":"()V",
"invocationCount":275
"className":"com/qunar/sa/TargetProcess$Student",
"methodName":"JITTest2",
"parameters":"()V",
"invocationCount":512
"className":"com/qunar/sa/TargetProcess$Student",
"methodName":"setId",
"parameters":"(I)V",
"invocationCount":1
SA遍历热编译数据:[
"className":"com.qunar.sa.TargetProcess$Student",
"methodName":"JITTest3",
"parameters":"",
"invocationCount":275
"className":"com.qunar.sa.TargetProcess$Student",
"methodName":"JITTest2",
"parameters":"",
"invocationCount":512
由以上执行结果可以反过来验证上面对 JIT 运行机制的分析,如:
1)int 型数据_counter 包括三部分,第3-31位表示执行次数,第2位表示是否已被编译1为编译,第0位和第1位表示超出阈值时的处理,默认情况为01即超出阈值执行编译,我们需要通过将 _counter 右移三位才能获取方法调用次数。
2)短时间内小于编译阈值的调用次数是准确的,如以上代码对 JITTest1 的调用
3)JVM 中的方法统计并非准确的调用次数,两个原因导致统计不准确:
触发 JIT 之后解释执行转换为编译执行,不再进行方法统计。
方法次数存在热度衰减机制。
8. jmap、jstack、 等 Tools 工具实现原理
其实,Java SA 离我们并不遥远,你是否用过 jmap、jstack、jinfo 等 jdk 自带的命令行工具?如果是那你可能已经在使用 Java SA 了,为什么说可能,是因为 jmap 等工具内置两种实现方式:attach 方式和 SA 方式。下面以 jmap 为例通过源码来分析如何选择这两种机制:
以下代码摘自:jdk/src/share/classes/sun/tools/jmap/JMap.java
* This class is the main class for the JMap utility. It parses its arguments
* and decides if the command should be satisfied using the VM attach mechanism
* or an SA tool. At this time the only option that uses the VM attach mechanism
* is the -dump option to get a heap dump of a running application. All other
* options are mapped to SA tools.
/public class JMap {
// Options handled by the attach mechanism
private static String HISTO_OPTION = "-histo";
private static String LIVE_HISTO_OPTION = "-histo:live";
private static String DUMP_OPTION_PREFIX = "-dump:";
// These options imply the use of a SA tool
private static String SA_TOOL_OPTIONS =
"-heap|-heap:format=b|-clstats|-finalizerinfo";
// The -F (force) option is currently not passed through to SA
private static String FORCE_SA_OPTION = "-F";
当参数带有-F 或-heap|-heap:format=b|-clstats|-finalizerinfo 时使用 Java SA 获取数据,参数带有-histo、-histo:live、-dump:时通过 attach 机制获取数据。这两种方式都是 hotspot JVM 为我们提供的进程间通信方式,实现机制却大相径庭。
8.1. attch 方式
采用“协作”模型,目标 JVM 启动时会启动 Signal Dispatcher 守护线程,jmap 命令执行时底层调用了 com.sun.tools.attach.VirtualMachine.attach(pid),jmap 进程会发出 SIGQUIT 信号,Signal Dispatcher 线程收到信号后就会创建 Attach Listener 线程,后续 jmap 进程继续执行
com.sun.tools.attach.VirtualMachine.executeCommand,此时两个进程建立 socket 连接,jmap 进程发送命令(对于 jmap -histo 发送的命令为:"inspectheap"),Attach Listener 线程接受该命令并委派给对应的函数执行处理,处理完成后通过 socket 写回,jmap 进程读取后展示到控制台。整体交互流程如下:
8.2. SA 方式
SA 机制不需要与进程互动,通过直接分析目标进程的内存布局获取目标 JVM 进程的运行时对象数据,jmap -heap 的调用流程如下:
下一个章节会围绕 SA 做详细分析。
9. Java SA 运行机制
9.1. SA 获取数据步骤
本小节通过一个运行的 pid 为11063的 Java 进程来探索 Java SA 底层运行机制,通过 Java SA 获取数据主要包括以下步骤:
1)一个 java 进程运行时会通过动态加载的方式加载 JVM 自己的动态共享库,JVM 的核心链接共享库是 libjvm.so,共享库使用 ELF 格式,运行时内核会把 ELF 加载到用户空间,其中就包含该共享库提供的符号表,符号表中记录了这个模块定义的可以提供给其他模块引用的全局符号。我们可以使用 linux 提供的 readelf 命令获取符号表,执行的命令及输出如下:
readelf -s /home/q/java/jdk1.8.0_60/jre/lib/amd64/server/libJVM.so|less
在结果中查找我们后续关注的全局变量 gHotSpotVMTypes,该变量及 gHotSpotVMStructs 变量是 SA 访问 JVM 其他所有变量的根基。其中 0000000000f8c4e8 为 gHotSpotVMTypes 全局变量的相对内存地址,8为符号占据的内存大小,OBJECT 标识当前符合的类型为对象,GLOBAL 标识该符号的作用范围为全局,即 gHotSpotVMTypes 为全局对象。\
2)第一步中获得的只是变量的相对地址偏移,并不是真实运行中的进程的内存地址,如何得到内存基址呢?回顾一下上文中 linux 进程内存布局的内容,linux 中进程的内存由一组 vma 表示,每一个动态共享库都会在内存映射区中被映射成一组 vma,而 linux 中查询 vma 的操作很简单,通过/proc/[pid]/maps 就可以获取目标 JVM 进程的所有 VMA 数据,我们从中取出我们关注的 libjvm.so 的 vma 数据,执行的命令及输出如下:
命令:sudo cat /proc/11063/maps |grep libJVM.so|less
7f4dfc945000-7f4dfd603000 r-xp 00000000 fc:07 1183253 /home/q/java/jdk1.8.0_60/jre/lib/amd64/server/libJVM.so
7f4dfd603000-7f4dfd802000 ---p 00cbe000 fc:07 1183253 /home/q/java/jdk1.8.0_60/jre/lib/amd64/server/libJVM.so
7f4dfd802000-7f4dfd8da000 rw-p 00cbd000 fc:07 1183253 /home/q/java/jdk1.8.0_60/jre/lib/amd64/server/libJVM.so
vma 数据结构如下:以下代码摘自:/include/linux/mm.h
struct vm_area_struct {
struct mm_struct * vm_mm;
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
pgprvm_page_ot;
unsigned long vm_flags;
... 此处省略部分源码
struct file * vm_file;
其中最后一行为该共享库的数据段,7f4dfd802000-7f4dfd8da000 表示 vmstart 开始地址-vmend 结束地址 ,/home/q/java/jdk1.8.060/jre/lib/amd64/server/libJVM.so 表示 vmfile 即映射文件,7f4dfd802000即数据段的起始地址就是全局变量 gHotSpotVMTypes 的基址。
3)将第一步获取的相对地址和第二步获取的内存基址相加就可以得到该变量的绝对虚拟地址。虚拟地址可通过页目录和页表转化为物理地址,此处不再赘述。
4)拿到该变量地址后就可以使用 ptrace 函数获取变量值了,最终调用的是 ptrace 系统调用,代码位置:hotspot/agent/src/os/linux/ps_proc.c
5)JVM 主要用 C++ 实现,其中的类多种多样、种类繁多,要实现进程间通讯,一个不得不考虑的问题就是:如何将如此众多的 C++ 类使用通用的数据结构在内存中表示出来,进而通过 SA 机制读取后转换为 Java 对象,解释这个问题就不得不提两个极其重要的 C++ 结构体:VMStructEntry 和 VMTypeEntry,和一个极其重要的 C++ 类:VMStructs,上述问题正是通过这两个结构体巧妙的解决的,代码位于:hotspot/src/share/vm/runtime/vmStructs.hpp
示例代码:
class SystemDictionary {
static Dictionary* _dictionary;
VMStructEntry和VMTypeEntry结构体中的注释以上面示例代码作为分析对象:
typedef struct {
const char* typeName;
const char* fieldName;
const char* typeString;
int32_t isStatic;
uint64_t offset;
void* address;
VMStructEntry;
typedef struct {
const char* typeName;
const char* superclassName;
int32_t isOopType;
int32_t isIntegerType;
int32_t isUnsigned;
uint64_t size;
} VMTypeEntry;
VMTypeEntry 是一个通用的对象表示,VMStructEntry 表示对象中的变量,其中后面的注释是以上面示例代码作为分析对象,如果是静态变量可以通过 VMStructEntry 中的 address 获取变量所在绝对地址(虚拟),如果是非静态变量,则需通过当前变量所在对象的地址 +offse 才能获取变量的地址
使用 VMTypeEntry 和 VMStructEntry 的代码 在hotspot/src/share/vm/runtime/vmStructs.cpp 中,部分核心注释及代码如下:
注释1:
注释2:
static_field(SystemDictionary, _dictionary, Dictionary*) \
hotspot 实现中的 vmStructs.cpp 和 Java SA 中的 sun.JVM.hotspot.HotSpotTypeDataBase 互相依赖,vmStructs.cpp 列出了所有可以通过 Java SA api 获取到的 JVM 数据,获取方法就是调用 HotSpotTypeDataBase 中的方法,依然以上面 SystemDictionary 部分的示例代码为例,使用 Java SA 获取 JVM 中_dictionary 对象的代码如下:
private static synchronized void initialize(TypeDataBase db) {
Type type = db.lookupType("SystemDictionary");
dictionaryField = type.getAddressField("_dictionary");
9.2 SA 数据获取步骤总结
注意:Java SA 虽然强大,到目前为止官方并不推荐使用 Java SA,相关 Api 在不同版本之间差异可能很大,使用 Java SA 的一个大前提是限定 JDK 版本。
usenix 对 Java SA 的介绍:static.usenix.org/event/JVM01…
openjdk 对 Java SA 的介绍:openjdk.java.net/groups/hots…
读取动态链接共享库文件中的符号表:blog.csdn.net/raintungli/…