文章目录
- 0、客户端代码
- 1、JMX
- 2、实现:查看内存使用情况
- 3、实现:查看直接内存
- 4、实现:生成堆内存快照
- 5、实现:打印栈信息
- 6、实现:打印类加载器的信息
- 7、实现:打印类的源码
- 8、需求:打印方法的耗时
自己写一个Arthas工具(简化版),功能点包括:
- 查看内存使用情况
- 生成堆内存快照
- 打印栈信息
- 打印类加载器
- 打印类的源码
- 打印方法执行的参数和耗时
提供一个独立的Jar,无侵入性,可用于任何Java程序:
0、客户端代码
获取所有的Java进程的ID,让用户只需选择PID,连接Java进程,加载Java Agent的Jar,进行动态代理:
public class AttachTest {
public static void main(String[] args) throws Exception {
//获取进程列表,让用户自己选择连接哪个PID
//执行jps指令
Process process = Runtime.getRuntime().exec("jps");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
try (bufferedReader) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
//用户输入进程ID
Scanner scanner = new Scanner(System.in);
String pid = scanner.next();
//连接用户输入的进程
VirtualMachine vm = VirtualMachine.attach(pid);
//执行Java Agent的里的agentmain方法
vm.loadAgent("D:\\jmh2\\llg-agent\\target\\llg-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}
1、JMX
JDK1.5起,提供Java Management Extensions (JMX) 技术,JMX技术使得开发者可以在内存中保存一个MXbean对象,存一些配置信息(类似对象容器的方式去存放一种特有的对象),另外,JVM也将一些程序的运行信息放入了MXbean对象。
简言之,通过JMX,写入或者读取MXbean,可实现:
- 运行时配置的获取和更改
- 获取应用程序的运行信息,如:线程栈、内存、类的信息
应用场景:
- VisualVM使用JMX技术远程连接的方式,由Java程序暴露一个端口,让VisualVM拿到MXbean对象的信息,做一个内存、线程等信息的展示
- 自定义一个JavaAgent,调用方法操作MXbean对象
关于JMX能调用的方法:
ManagementFactory.getMemoryPoolMXBeans() //获取内存信息
其他方法:
2、实现:查看内存使用情况
调用getMemoryPoolMXBeans方法,获取JVM各块内存对象的List,分堆和非堆打印:
public class MemoryCommand {
//打印所有的内存信息
public static void printMemory() {
//获取内存信息,返回List的结果,List中有伊甸园区、老年代、元空间等对象
//下面分堆和非堆,分开打印
List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();
System.out.println("堆内存:");
//堆内存
getMemoryInfo(memoryPoolMXBeans,MemoryType.HEAP);
System.out.println("非堆内存:");
//非堆内存
getMemoryInfo(memoryPoolMXBeans,MemoryType.NON_HEAP);
}
/**
* 处理内存信息
* @param memoryPoolMXBeans 内存信息
* @param heapType 堆或非堆
*/
public static void getMemoryInfo(List<MemoryPoolMXBean> memoryPoolMXBeans,MemoryType heapType){
memoryPoolMXBeans.stream().filter(x -> x.getType().equals(heapType))
.forEach(x -> {
StringBuilder sb = new StringBuilder();
sb.append("name:")
.append(x.getName())
//使用量used
.append(" used:")
.append(x.getUsage().getUsed() / 1024 / 1024) //byte转M
.append("M")
//申请量total
.append(" committed:")
.append(x.getUsage().getCommitted() / 1024 / 1024)
.append("M")
//最大值max
.append(" max:")
.append(x.getUsage().getMax() / 1024 / 1024)
.append("M");
System.out.println(sb);
});
}
}
改下agentmain方法的逻辑,调用上面打印内存信息的方法:
public class AgentMain {
public static void agentmain(String agentArgs,Instrumentation inst){
MemoryCommand.printMemory();
}
}
连接一个PID试试:
3、实现:查看直接内存
继续用JMX技术来实现:
//加载这个类(它里面包含直接内存的使用情况),获取class对象
Class bufferPoolMXBeanClass = Class.forName("java.lang.management.BufferPoolMXBean");
//getPlatformMXBeans允许传入一个MXbean的Class对象,并获取到这个MXbean,因为可能有多个,所以返回一个List
List<BufferPoolMXBean>bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(bufferPoolMXBeanclass)
如此,通过BufferPoolMXBean的这个MXbean,可获取JVM中分配的直接内存和内存映射缓冲区(这个区用于提升大文件读写性能)等的大小。具体实现:
/**
* 打印nio相关的内存
*/
public static void printDirectMemory() {
try {
Class clazz = Class.forName("java.lang.management.BufferPoolMXBean");
List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(clazz);
//打印内容
for (BufferPoolMXBean mxBean : bufferPoolMXBeans) {
StringBuilder sb = new StringBuilder();
sb.append("name:")
.append(mxBean.getName())
//使用量used
.append(" used:")
.append(mxBean.getMemoryUsed() / 1024 / 1024) //byte转M
.append("M")
//容量
.append(" capacity:")
.append(mxBean.getTotalCapacity() / 1024 / 1024)
.append("M");
System.out.println(sb);
}
} catch (Exception e) {
e.printStackTrace();
}
}
将这个方法直接在上面打印堆和非堆的方法里调一下。给用户的程序分配100M的直接内存,
动态代理一下,
看看效果:
4、实现:生成堆内存快照
关于生成内存快照:依旧调用getPlatformMXBean
//获取HotSpot虚拟机诊断用的一个MXbean对象,用这个Bean可以生成内存快照
//这里已知这个MXbean只有一个,所以掉没有s的方法,不再像上面直接内存一样返回一个List
HotspotDiagnosticMXBean hotspotDiagnosticMXBean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMxBean.class);
具体实现:
/**
* 生成内存快照
*/
public static void heapDump(){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
HotSpotDiagnosticMXBean mXBean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
//参数二,选true即只需要dump存活的对象,
try {
mXBean.dumpHeap(dateFormat.format(new Date()) + ".hprof",true);
} catch (IOException e) {
System.out.println("快照导出失败");
e.printStackTrace();
}
}
agentmain中调用一下:
public class AgentMain {
public static void agentmain(String agentArgs,Instrumentation inst){
//打印内存
//MemoryCommand.printMemory();
//导出内存快照
MemoryCommand.heapDump();
}
}
输入普通应用的PID,动态代理一下:
导出快照文件成功:
5、实现:打印栈信息
用JMX的方法,还是通过对应的MXBean来获取
ManagementFactory.getThreadMXBean()
实现:
public class ThreadCommand {
/**
* 打印栈信息
*/
public static void printThreadInfo() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//参数一二分别为:当前的虚拟机VM是否能支持监视器和同步器,重载时的第三个参数是栈的深度(不传,默认是Int的最大值,如此,展示的栈信息最全,但性能不好)
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported()
, threadMXBean.isSynchronizerUsageSupported());
//打印线程信息,ThreadInfo对象中包括了栈名称、方法的调用等,按需自取
for (ThreadInfo threadInfo : threadInfos) {
//线程信息
StringBuilder builder = new StringBuilder();
builder.append("name: ")
.append(threadInfo.getThreadName())
.append(" threadId: ")
.append(threadInfo.getThreadId())
.append(" threadState: ")
.append(threadInfo.getThreadState());
System.out.println(builder);
//方法调用栈
StackTraceElement[] stackTrace = threadInfo.getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
System.out.println(stackTraceElement);
}
}
}
}
agentmain中调用一下:
public class AgentMain {
public static void agentmain(String agentArgs,Instrumentation inst){
//打印内存
//MemoryCommand.printMemory();
//导出内存快照
//MemoryCommand.heapDump();
//打印栈信息
ThreadCommand.printThreadInfo();
}
}
输入普通应用的PID,动态代理一下:
打印成功:
6、实现:打印类加载器的信息
Java Agent中可以获得通过Java虚拟机提供的Instumentation
对象获取类和类加载器的信息
作用:
- redefine:重新设置类的字节码信息(Arthas热部署应该就用到了它)
- retransform:根据现有类的字节码信息进行增强
- 获取所有已加载的类信息
//相关文档
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html
具体实现:
public class ClassCommand {
/**
* 打印所有的类加载器
* 形参直接用Instrumentation对象,在premain或者agentmain方法中,JDK会自己注入
*/
public static void printAllClassLoader(Instrumentation inst) {
//获取所有已加载的类
Class[] allLoadedClasses = inst.getAllLoadedClasses();
//用于去重,因为类加载器就那几种
Set<ClassLoader> classLoaderSet = new HashSet<>();
for (Class loadedClass : allLoadedClasses) {
ClassLoader classLoader = loadedClass.getClassLoader();
classLoaderSet.add(classLoader);
}
//打印类加载器
String classLoaderInfo = classLoaderSet.stream()
.map(x -> {
//获取启动类加载器的结果为null,这里我直接给个固定的名字
if (x == null) {
return "BootStrapClassLoader";
}
//其他的类加载器就正常输出
return x.getName();
})
//类加载器名字为空的不要
.filter(x -> x != null)
.distinct()
.sorted(String::compareTo)
.collect(Collectors.joining(","));
System.out.println(classLoaderInfo);
}
}
agentmain中调用一下:
public class AgentMain {
public static void agentmain(String agentArgs,Instrumentation inst){
//打印内存
//MemoryCommand.printMemory();
//导出内存快照
//MemoryCommand.heapDump();
//打印栈信息
//ThreadCommand.printThreadInfo();
//打印类加载器信息
ClassCommand.printAllClassLoader(inst);
}
}
输入普通应用的PID,动态代理一下,普通应用打印它的类加载器成功:
7、实现:打印类的源码
思路:内存中存的是类的字节码信息,用Instumentation
对象提供的转换器获取字节码信息
并用反编译工具jd-core
得到源码:
//参考
https://github.com/java-decompiler/jd-core
使用jd-core,copy官方示例,Loader注意改字节码的来源,Printer重写end方法,打印反编译后的源码即可
jd-core的依赖:
<dependency>
<groupId>org.jd</groupId>
<artifactId>jd-core</artifactId>
<version>1.1.3</version>
</dependency>
具体实现:
public class ClassCommand {
/**
* 打印类的源代码
*/
public static void printClassSourceCode(Instrumentation inst) {
//输入全类名
System.out.println("请输入全类名:");
Scanner scanner = new Scanner(System.in);
String className = scanner.next();
//获取所有已加载的类,从中找到用户要的类的class对象
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class loadedClass : allLoadedClasses) {
//找到了
if (className.equals(loadedClass.getName())) {
//转换器
ClassFileTransformer transformer = new ClassFileTransformer() {
@Override
//transform方法传入一个字节码信息,返回一个增强后的字节码信息,以下代码的写法,返回null即不增强,这里只要原来的字节码
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//通过jd-code反编译,打印出源码
printJdCoreSourceCode(classfileBuffer, className);
return ClassFileTransformer.super.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); //直接调用以有的父类,返回null,即不改,因为这里只要获取最初的字节码
}
};
//添加转换器,让转换器生效,参数二为true即可手动触发
inst.addTransformer(transformer, true);
//触发转换器
try {
inst.retransformClasses(loadedClass);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
} finally {
//使用完之后删除转换器
inst.removeTransformer(transformer);
}
}
}
}
/**
* jd-code打印源码
* bytes 字节码信息
*/
private static void printJdCoreSourceCode(byte[] bytes, String className) {
//loader对象
Loader loader = new Loader() {
@Override
public byte[] load(String internalName) throws LoaderException {
return bytes;
}
@Override
public boolean canLoad(String internalName) {
return true; //类可加载
}
};
//Printer对象,注意重写end方法,打印源代码
Printer printer = new Printer() {
protected static final String TAB = " ";
protected static final String NEWLINE = "\n";
protected int indentationCount = 0;
protected StringBuilder sb = new StringBuilder();
@Override
public String toString() {
return sb.toString();
}
@Override
public void start(int maxLineNumber, int majorVersion, int minorVersion) {
}
@Override
public void end() {
//打印源代码
System.out.println(sb);
}
@Override
public void printText(String text) {
sb.append(text);
}
@Override
public void printNumericConstant(String constant) {
sb.append(constant);
}
@Override
public void printStringConstant(String constant, String ownerInternalName) {
sb.append(constant);
}
@Override
public void printKeyword(String keyword) {
sb.append(keyword);
}
@Override
public void printDeclaration(int type, String internalTypeName, String name, String descriptor) {
sb.append(name);
}
@Override
public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) {
sb.append(name);
}
@Override
public void indent() {
this.indentationCount++;
}
@Override
public void unindent() {
this.indentationCount--;
}
@Override
public void startLine(int lineNumber) {
for (int i = 0; i < indentationCount; i++) sb.append(TAB);
}
@Override
public void endLine() {
sb.append(NEWLINE);
}
@Override
public void extraLine(int count) {
while (count-- > 0) sb.append(NEWLINE);
}
@Override
public void startMarker(int type) {
}
@Override
public void endMarker(int type) {
}
};
//通过jd-code打印
ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler();
try {
decompiler.decompile(loader, printer, className);
} catch (Exception e) {
e.printStackTrace();
}
}
}
agentmain中调用一下:
public class AgentMain {
public static void agentmain(String agentArgs,Instrumentation inst){
//打印内存
//MemoryCommand.printMemory();
//导出内存快照
//MemoryCommand.heapDump();
//打印栈信息
//ThreadCommand.printThreadInfo();
//打印类加载器信息
//ClassCommand.printAllClassLoader(inst);
//打印源码
ClassCommand.printClassSourceCode(inst);
}
}
输入普通应用的PID,动态代理一下,源码打印成功:
8、需求:打印方法的耗时
打印方法执行的参数和耗时,就需要对原始方法做增强。这里用字节码增强框架ASM和ByteBuddy实现。(不用Java Agent,不考虑无侵入式的话可以在自己项目中用Spring AOP,通过切面+代理对象实现)