TProfiler是一个能够在生产环境长期使用的性能分析工具.它同时支持剖析和采样两种方式,记录方法执行的时间和次数,生成方法热点 对象建立热点 线程状态分析等数据,为查找系统性能瓶颈提供数据支持.
TProfiler在JVM启动时把时间采集程序注入到字节码中,整个过程无需修改应用源码.运行时会把数据写到日志文件,通常状况下每小时输出的日志小于50M.
业界同类开源产品都不是针对大型Web应用设计的,对性能消耗较大不能长期使用,TProfiler解决了这个问题.目前TProfiler已应用于淘宝的核心Java前端系统.
部署后低峰期对应用响应时间影响20% 高峰期对吞吐量大约有30%的下降(高峰期能够远程关闭此工具)
前端
与同类开源工具jip对比 | ||
---|---|---|
项目 | TProfiler | JIP |
交互控制java |
支持远程开关和状态查看git |
支持远程开关等多种操做github |
过滤包和类名web |
支持包和类的过滤编程 |
支持包和类的过滤数组 |
低消耗数据结构 |
响应时间延长20% QPS下降30%(详细对比看上图)框架 |
同等条件下资源消耗较多,使JVM不断的FullGC;Profile时会阻塞其余线程异步 |
无本地代码 |
未使用JVMTI,纯Java开发 |
未使用JVMTI,纯Java开发 |
易用性 |
只有一个jar包,使用简单 |
模块多,配置使用相对复杂 |
日志文件 |
对日志进行优化,每小时通常小于50M |
同等条件下日志大约是TProfiler的8倍,不能自动dump须要客户端触发 |
日志分析 |
目前只提供文本展现 |
能够利用客户端分析展现日志 |
使用场景 |
大型应用/小型应用 长期使用 |
小型应用 短时间使用 |
从图中能够看出,该工具使用了java6的Instrumention特性以及asm字节码修改
利用 Java 代码,即 java.lang.instrument 作动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之能够用 Java 代码的方式解决问题。使用 Instrumentation,开发者能够构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至可以替换和修改某些类的定义。有了这样的功能,开发者就能够实现更为灵活的运行时虚拟机监控和 Java 类操做了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 作任何升级和改动,就能够实现某些 AOP 的功能了。
在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具备了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。
Instrumentation 的基本功能和用法
“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了之前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,能够支持第三方工具程序以代理的方式链接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成不少跟 JVM 相关的功能。事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,经过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操做。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操做等等方面提供了大量有价值的函数。关于 JVMTI 的详细信息,请参考 Java SE 6 文档(请参见 参考资源)当中的介绍。
Instrumentation 的最大做用,就是类定义动态改变和操做。在 Java SE 5 及其后续版本当中,开发者能够在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,经过 – javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
在 Java SE 5 当中,开发者可让 Instrumentation 代理在 main 函数运行前执行。简要说来就是以下几个步骤:
1:编写 premain 函数
public static void premain(String agentArgs, Instrumentation inst); [1] public static void premain(String agentArgs); [2]
其中,[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。
在这个 premain 函数中,开发者能够进行对类的各类操做。
agentArgs 是 premain 函数获得的程序参数,随同 “– javaagent”一块儿传入。与 main 函数不一样的是,这个参数是一个字符串而不是一个字符串数组,若是程序参数有多个,程序将自行解析这个字符串。
Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎全部的功能方法,例如类定义的转换和操做等等。
jar 文件打包
2:将这个 Java 类打包成一个 jar 文件,并在其中的 manifest 属性当中加入” Premain-Class”来指定步骤 1 当中编写的那个带有 premain 的 Java 类。(可能还须要指定其余属性以开启更多功能)
3:运行
用以下方式运行带有 Instrumentation 的 Java 程序:
java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]
对 Java 类文件的操做,能够理解为对一个 byte 数组的操做(将类文件的二进制字节流读入一个 byte 数组)。开发者能够在“ClassFileTransformer”的 transform 方法当中获得,操做并最终返回一个类的定义(一个 byte 数组)。
Tprofiler就是经过这种方式来对字节码进行修改的。
首先假设咱们有一个类HelloWorld, 能够经过一个方法打印字符串。
public HelloWorld{ public void sayHello(){ System.out.println("hello world!"); } }
咱们须要记录方法执行的时间和次数须要如何实现?
ProfTransformer是Tprofiler自定义的ClassFileTransformer,用于转换类字节码
public class ProfTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (loader != null && ProfFilter.isNotNeedInjectClassLoader(loader.getClass().getName())) { return classfileBuffer; } if (!ProfFilter.isNeedInject(className)) { return classfileBuffer; } if (ProfFilter.isNotNeedInject(className)) { return classfileBuffer; } if (Manager.instance().isDebugMode()) { System.out.println(" ---- TProfiler Debug: ClassLoader:" + loader + " ---- class: " + className); } // 记录注入类数 Profiler.instrumentClassCount.getAndIncrement(); try { ClassReader reader = new ClassReader(classfileBuffer); ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter adapter = new ProfClassAdapter(writer, className); reader.accept(adapter, 0); // 生成新类字节码 return writer.toByteArray(); } catch (Exception e) { e.printStackTrace(); // 返回旧类字节码 return classfileBuffer; } } }
这个类实现了 ClassFileTransformer 接口。其中 ClassFileTransformer 当中规定的 transform 方法则完成了类定义的替换转换。
在代码中咱们能够看到两个类ClassReader和ClassWriter这两个ASM的字节码操做API,经过他们咱们能够对原来的class进行增长行为。
(ASM是一个操做字节码(bytecode)的框架,很是的小巧和快速,这个asm-3.3.1.jar,只有43k的大小。asm提供了字节码的读写的功能。而asm的核心,采用的是visitor的模式,提供了ClassReader和ClassWriter这两个很是重要的类以及ClassVisitor这个核心的接口。)
ASM框架中的核心类有如下几个:
ClassReader:该类用来解析编译过的class字节码文件。
ClassWriter:该类用来从新构建编译后的类,好比说修改类名、属性以及方法,甚至能够生成新的类的字节码文件。
ClassAdapter:该类也实现了ClassVisitor接口,它将对它的方法调用委托给另外一个ClassVisitor对象。
ClassReader的职责是读取字节码。能够用InputStream、byte数组、类名(须要ClassLoader.getSystemResourceAsStream可以加载到的class文件)做为构造函数的参数构造ClassReader对象,来读取字节码。而ClassReader的工做,就是根据字节码的规范,从输入中读取bytecode。而经过ClassReader对象,获取bytecode信息有两种方式,一种就是采用visitor的模式,传入一个ClassVisitor对象给ClassReader的accept方法。另一种,是使用Low Level的方式,使用ClassReader提供了readXXX以及getXXX的方法来获取信息。
对于通常使用,用ClassReader的accept方法,使用visitor模式就能够了。其中ProfClassAdapter继承了org.objectweb.asm.ClassAdapter。在asm中,ClassAdapter这个类,用asm的javadoc上的话说,就是一个代理到其余ClassVisitor的一个空的ClassVisitor(An empty ClassVisitor that delegates to another ClassVisitor.)。具体来讲,构造ClassAdapter对象的时候,须要传递一个ClassVisitor的对象给ClassAdapter的构造函数,而ClassAdapter对ClassVisitor的实现,就是直接调用这个传给ClassAdapter的ClassVisitor对象的对应visit方法。ClassVisitor,在 ASM3.0 中是一个接口,到了 ASM4.0 与 ClassAdapter 抽象类合并。主要负责 “拜访” 类成员信息。其中包括(标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块)
这里主要介绍其中几个关键方法:
visit(int , int , String , String , String , String[])
该方法是当扫描类时第一个拜访的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 , 泛型信息 , 继承的父类 , 实现的接口)。
visitAnnotation(String , boolean)
该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否能够在 JVM 中可见)。
visitField(int , String , String , String , Object)
该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)。
visitMethod(int , String , String , String , String[])
该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 , 泛型信息 , 抛出的异常)。
visitEnd()
该方法是当扫描器完成类扫描时才会调用,若是想在类中追加某些方法。能够在该方法中实现。在后续文章中咱们会用到这个方法。
接下来咱们看在ProfClassAdapter中作了些什么:
public class ProfClassAdapter extends ClassAdapter { /** * 类名 */ private String mClassName; /** * 文件名 */ private String mFileName = null; /** * 字段对应方法列表 */ private List<String> fieldNameList = new ArrayList<String>(); /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visit(int, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[]) */ public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); } /** * @param visitor * @param theClass */ public ProfClassAdapter(ClassVisitor visitor, String theClass) { super(visitor); this.mClassName = theClass; } /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visitSource(java.lang.String, java.lang.String) */ public void visitSource(final String source, final String debug) { super.visitSource(source, debug); mFileName = source; } /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visitField(int, java.lang.String, java.lang.String, java.lang.String, java.lang.Object) */ public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { String up = name.substring(0, 1).toUpperCase() + name.substring(1, name.length()); String getFieldName = "get" + up; String setFieldName = "set" + up; String isFieldName = "is" + up; fieldNameList.add(getFieldName); fieldNameList.add(setFieldName); fieldNameList.add(isFieldName); return super.visitField(access, name, desc, signature, value); } /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[]) */ public MethodVisitor visitMethod(int arg, String name, String descriptor, String signature, String[] exceptions) { if (Manager.isIgnoreGetSetMethod()) { if (fieldNameList.contains(name)) { return super.visitMethod(arg, name, descriptor, signature, exceptions); } } // 静态区域不注入 if ("<clinit>".equals(name)) { return super.visitMethod(arg, name, descriptor, signature, exceptions); } MethodVisitor mv = super.visitMethod(arg, name, descriptor, signature, exceptions); MethodAdapter ma = new ProfMethodAdapter(mv, mFileName, mClassName, name); return ma; } }
他主要重写visitField和visitMethod这两个方法,其中visitField中主要作的事情就是获得属性名称的get和set方法名称放入集合中,在visitMethod调用的时候先判断是否过滤get和set方法,不对他们进行加强操做。在执行MethodVisitor mv = super.visitMethod(arg, name, descriptor, signature, exceptions);会得到MethodVisitor对象,经过这个对象能够对方法访问进行AOP的代码注入。
public class ProfMethodAdapter extends MethodAdapter { /** * 方法ID */ private int mMethodId = 0; /** * @param visitor * @param fileName * @param className * @param methodName */ public ProfMethodAdapter(MethodVisitor visitor, String fileName, String className, String methodName) { super(visitor); mMethodId = MethodCache.Request(); MethodCache.UpdateMethodName(mMethodId, fileName, className, methodName); // 记录方法数 Profiler.instrumentMethodCount.getAndIncrement(); } /* (non-Javadoc) * @see org.objectweb.asm.MethodAdapter#visitCode() */ public void visitCode() { this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "Start", "(I)V"); super.visitCode(); } /* (non-Javadoc) * @see org.objectweb.asm.MethodAdapter#visitLineNumber(int, org.objectweb.asm.Label) */ public void visitLineNumber(final int line, final Label start) { MethodCache.UpdateLineNum(mMethodId, line); super.visitLineNumber(line, start); } /* (non-Javadoc) * @see org.objectweb.asm.MethodAdapter#visitInsn(int) */ public void visitInsn(int inst) { switch (inst) { case Opcodes.ARETURN: case Opcodes.DRETURN: case Opcodes.FRETURN: case Opcodes.IRETURN: case Opcodes.LRETURN: case Opcodes.RETURN: case Opcodes.ATHROW: this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "End", "(I)V"); break; default: break; } super.visitInsn(inst); } }
MethodAdapter类实现了MethodVisitor接口,在MethodVisitor接口中严格地规定了每一个visitXXX的访问顺序,以下:
visitAnnotationDefault?( visitAnnotation | visitParameterAnnotation | visitAttribute )*( visitCode( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |visitLocalVariable | visitLineNumber )*visitMaxs )?visitEnd这里所说的ASM访问不是指ASM代码去调用某个类的具体方法,而是指去分析某个类的某个方法的二进制字节码。
在这里visitCode方法将会在ASM开始访问某一个方法时调用,所以这个方法通常能够用来在进入分析JVM字节码以前来新增一些字节码,visitXxxInsn是在ASM具体访问到每一个指令时被调用,上面代码中咱们使用的是visitInsn方法,它是ASM访问到无参数指令时调用的,这里咱们判但了当前指令是否为无参数的return来在方法结束前添加一些指令。
经过重写visitCode和visitInsn两个方法,咱们就实现了具体的业务逻辑被调用前和被调用后植入监控运行时间的代码。
咱们看重写里面作了什么:
public ProfMethodAdapter(MethodVisitor visitor, String fileName, String className, String methodName) { super(visitor); mMethodId = MethodCache.Request(); MethodCache.UpdateMethodName(mMethodId, fileName, className, methodName); // 记录方法数 Profiler.instrumentMethodCount.getAndIncrement(); }
这个构造函数里面首先调用了MethodCache.Request()占位并生成方法ID,而后初始化方法信息。
接着是
public void visitCode() { this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "Start", "(I)V"); super.visitCode(); }
重写这个方法实现了在访问方法前执行Profiler的静态方法start,用于开启时间以及方法信息记录逻辑。
在而后是
public void visitInsn(int inst) { switch (inst) { case Opcodes.ARETURN: case Opcodes.DRETURN: case Opcodes.FRETURN: case Opcodes.IRETURN: case Opcodes.LRETURN: case Opcodes.RETURN: case Opcodes.ATHROW: this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "End", "(I)V"); break; default: break; } super.visitInsn(inst); }
表示在方法执行结束后执行Profiler的静态方法End,完成方法调用时间及其余数据的统计计算。
经过以上方式实现了无变成的AOP注入。
结下来咱们重点看下Profiler的start和end方法是如何将方法作记录的。
首先回到开始Tprofiler的实现原理中的那张图片:
其中使用了一个ThreadData数据用来记录线程性能分析数据。每一个线程有本身的ThreadData
ThreadData的数据结构:
public class ThreadData { /** * 性能分析数据 */ public ProfStack<long[]> profileData = new ProfStack<long[]>(); /** * 栈帧 */ public ProfStack<long[]> stackFrame = new ProfStack<long[]>(); /** * 当前栈深度 */ public int stackNum = 0; /** * 清空数据 */ public void clear(){ profileData.clear(); stackFrame.clear(); stackNum = 0;ssss } }
其中stackFrame是一个自定义栈的数组,包含三个数据,
frameData[0] = methodId;方法ID
frameData[1] = thrData.stackNum;调用数
frameData[2] = startTime;开始时间
thrData.stackFrame.push(frameData);
thrData.stackNum++;
这样在执行1个方法的前会作一些记录,并放入自定义栈
在来看看end作了什么:
/** * 方法退出时调用,采集结束时间 * * @param methodId */ public static void End(int methodId) { if (!Manager.instance().canProfile()) { return; } long threadId = Thread.currentThread().getId(); if (threadId >= size) { return; } long endTime; if (Manager.isNeedNanoTime()) { endTime = System.nanoTime(); } else { endTime = System.currentTimeMillis(); } try { ThreadData thrData = threadProfile[(int) threadId]; if (thrData == null || thrData.stackNum <= 0 || thrData.stackFrame.size() == 0) { // 没有执行start,直接执行end/多是异步中止致使的 return; } // 栈太深则抛弃部分数据 if (thrData.profileData.size() > 20000) { thrData.stackNum--; thrData.stackFrame.pop(); return; } thrData.stackNum--; long[] frameData = thrData.stackFrame.pop(); long id = frameData[0]; if (methodId != id) { return; } long useTime = endTime - frameData[2]; if (Manager.isNeedNanoTime()) { if (useTime > 500000) { frameData[2] = useTime; thrData.profileData.push(frameData); } } else if (useTime > 1) { frameData[2] = useTime; thrData.profileData.push(frameData); } } catch (Exception e) { e.printStackTrace(); } }
其实挺简单的,就是在方法执行结束后出栈,获取到其对应的开始记录的数据,而后作统计和计算,在吧结果放入到性能分析数据中去。 大致上这个就是Tprofiler的实现原理。至于在外面如何得到这些数据,在后续的文章中在说吧。