如何测量线程堆栈深度?

我有一个带有可伸缩性问题的32位Java服务:由于线程计数过多,用户数量很多,因此内存不足。 从长远来看,我计划切换到64位并减少每用户线程数。 在短期内,我想减少堆栈大小(-Xss,-XX:ThreadStackSize)以获得更多的空间。 但这是有风险的,因为如果我把它做得太小,我将得到StackOverflowErrors。

如何衡量应用程序的平均和最大堆栈大小,以指导我决定最佳-Xss值? 我对两种可能的方法感兴趣:

  1. 在集成测试期间测量正在运行的JVM。 哪些分析工具将报告最大堆栈深度?
  2. 寻找深度调用层次结构的应用程序的静态分析。 dependency injection的反思使得这不可行。

更新 :我知道解决此问题的长期正确方法。 请关注我问过的问题:如何测量堆栈深度?

更新2 :关于JProfiler的相关问题,我得到了一个很好的答案: JProfiler可以测量堆栈深度吗? (我根据JProfiler的社区支持建议发布了单独的问题)

您可以通过类似于可以编织到代码中的方面来了解堆栈深度(加载时间编织器以允许建议除系统类加载器之外的所有加载代码)。 该方面将解决所有已执行的代码,并且能够在您调用方法和返回时注意到。 您可以使用它来捕获大部分堆栈使用情况(您将错过从系统类加载器加载的任何内容,例如java。*)。 虽然不完美,但它避免了必须更改代码以在采样点收集StackTraceElement []并且还会使您进入可能未编写的非jdk代码。

例如(aspectj):

public aspect CallStackAdvice { pointcut allMethods() : execution(* *(..)) && !within(CallStackLog); Object around(): allMethods(){ String called = thisJoinPoint.getSignature ().toLongString (); CallStackLog.calling ( called ); try { return proceed(); } finally { CallStackLog.exiting ( called ); } } } public class CallStackLog { private CallStackLog () {} private static ThreadLocal> curStack = new ThreadLocal> () { @Override protected ArrayDeque initialValue () { return new ArrayDeque (); } }; private static ThreadLocal ascending = new ThreadLocal () { @Override protected Boolean initialValue () { return true; } }; private static ConcurrentHashMap> stacks = new ConcurrentHashMap> (); public static void calling ( String signature ) { ascending.set ( true ); curStack.get ().push ( signature.intern () ); } public static void exiting ( String signature ) { ArrayDeque cur = curStack.get (); if ( ascending.get () ) { ArrayDeque clon = cur.clone (); stacks.put ( hash ( clon ), clon ); } cur.pop (); ascending.set ( false ); } public static Integer hash ( ArrayDeque a ) { //simplistic and wrong but ok for example int h = 0; for ( String s : a ) { h += ( 31 * s.hashCode () ); } return h; } public static void dumpStacks(){ //implement something to print or retrieve or use stacks } } 

示例堆栈可能类似于:

 net.sourceforge.jtds.jdbc.TdsCore net.sourceforge.jtds.jdbc.JtdsStatement.getTds() public boolean net.sourceforge.jtds.jdbc.JtdsResultSet.next() public void net.sourceforge.jtds.jdbc.JtdsResultSet.close() public java.sql.Connection net.sourceforge.jtds.jdbc.Driver.connect(java.lang.String, java.util.Properties) public void phil.RandomStackGen.MyRunnable.run() 

非常慢并且有自己的内存问题,但可以为您提供所需的堆栈信息。

然后,您可以对堆栈跟踪中的每个方法使用max_stack和max_locals来计算方法的帧大小(请参阅类文件格式 )。 根据vm规范,我认为对于方法的最大帧大小应该是(max_stack + max_locals)* 4bytes(long / double占用操作数堆栈/本地变量上的两个条目,并且在max_stack和max_locals中占用)。

如果您的调用堆栈中没有那么多,您可以轻松地对感兴趣的类进行javap并查看帧值。 像asm这样的东西为您提供了一些简单的工具,可以用来更大规模地完成这项工作。

计算完成后,需要估计可能在最大堆栈点调用的JDK类的其他堆栈帧,并将其添加到堆栈大小。 它不会是完美的,但它应该为你提供一个不错的起点-Xss调整而不会破坏JVM / JDK。

另一个注意事项:我不知道JIT / OSR对帧大小或堆栈要求的作用,因此请注意,对于冷和热JVM上的-Xss调优可能会产生不同的影响。

编辑有几个小时的停机时间,并采取了另一种方法。 这是一个java代理,它将检测方法以跟踪最大堆栈帧大小和堆栈深度。 这将能够检测大多数jdk类以及其他代码和库,从而为您提供比方面编织器更好的结果。 你需要asm v4才能工作。 它更多的是为了它的乐趣所以在plinking java中为了好玩而不是利润而提交。

首先,制作一些东西来跟踪堆栈框架的大小和深度:

 package phil.agent; public class MaxStackLog { private static ThreadLocal curStackSize = new ThreadLocal () { @Override protected Integer initialValue () { return 0; } }; private static ThreadLocal curStackDepth = new ThreadLocal () { @Override protected Integer initialValue () { return 0; } }; private static ThreadLocal ascending = new ThreadLocal () { @Override protected Boolean initialValue () { return true; } }; private static ConcurrentHashMap maxSizes = new ConcurrentHashMap (); private static ConcurrentHashMap maxDepth = new ConcurrentHashMap (); private MaxStackLog () { } public static void enter ( int frameSize ) { ascending.set ( true ); curStackSize.set ( curStackSize.get () + frameSize ); curStackDepth.set ( curStackDepth.get () + 1 ); } public static void exit ( int frameSize ) { int cur = curStackSize.get (); int curDepth = curStackDepth.get (); if ( ascending.get () ) { long id = Thread.currentThread ().getId (); Integer max = maxSizes.get ( id ); if ( max == null || cur > max ) { maxSizes.put ( id, cur ); } max = maxDepth.get ( id ); if ( max == null || curDepth > max ) { maxDepth.put ( id, curDepth ); } } ascending.set ( false ); curStackSize.set ( cur - frameSize ); curStackDepth.set ( curDepth - 1 ); } public static void dumpMax () { int max = 0; for ( int i : maxSizes.values () ) { max = Math.max ( i, max ); } System.out.println ( "Max stack frame size accummulated: " + max ); max = 0; for ( int i : maxDepth.values () ) { max = Math.max ( i, max ); } System.out.println ( "Max stack depth: " + max ); } } 

接下来,制作java代理:

 package phil.agent; public class Agent { public static void premain ( String agentArguments, Instrumentation ins ) { try { ins.appendToBootstrapClassLoaderSearch ( new JarFile ( new File ( "path/to/Agent.jar" ) ) ); } catch ( IOException e ) { e.printStackTrace (); } ins.addTransformer ( new Transformer (), true ); Class[] classes = ins.getAllLoadedClasses (); int len = classes.length; for ( int i = 0; i < len; i++ ) { Class clazz = classes[i]; String name = clazz != null ? clazz.getCanonicalName () : null; try { if ( name != null && !clazz.isArray () && !clazz.isPrimitive () && !clazz.isInterface () && !name.equals ( "java.lang.Long" ) && !name.equals ( "java.lang.Boolean" ) && !name.equals ( "java.lang.Integer" ) && !name.equals ( "java.lang.Double" ) && !name.equals ( "java.lang.Float" ) && !name.equals ( "java.lang.Number" ) && !name.equals ( "java.lang.Class" ) && !name.equals ( "java.lang.Byte" ) && !name.equals ( "java.lang.Void" ) && !name.equals ( "java.lang.Short" ) && !name.equals ( "java.lang.System" ) && !name.equals ( "java.lang.Runtime" ) && !name.equals ( "java.lang.Compiler" ) && !name.equals ( "java.lang.StackTraceElement" ) && !name.startsWith ( "java.lang.ThreadLocal" ) && !name.startsWith ( "sun." ) && !name.startsWith ( "java.security." ) && !name.startsWith ( "java.lang.ref." ) && !name.startsWith ( "java.lang.ClassLoader" ) && !name.startsWith ( "java.util.concurrent.atomic" ) && !name.startsWith ( "java.util.concurrent.ConcurrentHashMap" ) && !name.startsWith ( "java.util.concurrent.locks." ) && !name.startsWith ( "phil.agent." ) ) { ins.retransformClasses ( clazz ); } } catch ( Throwable e ) { System.err.println ( "Cant modify: " + name ); } } Runtime.getRuntime ().addShutdownHook ( new Thread () { @Override public void run () { MaxStackLog.dumpMax (); } } ); } } 

agent类具有用于检测的premain钩子。 在该钩子中,它添加了一个类变换器,用于监视堆栈帧大小跟踪。 它还将代理添加到引导类加载器,以便它也可以处理jdk类。 要做到这一点,我们需要重新转换可能已经加载的任何东西,比如String.class。 但是,我们必须排除代理程序使用的各种内容或堆栈日志记录导致无限循环或其他问题(其中一些是通过反复试验找到的)。 最后,代理添加了一个关闭钩子以将结果转储到stdout。

 public class Transformer implements ClassFileTransformer { @Override public byte[] transform ( ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer ) throws IllegalClassFormatException { if ( className.startsWith ( "phil/agent" ) ) { return classfileBuffer; } byte[] result = classfileBuffer; ClassReader reader = new ClassReader ( classfileBuffer ); MaxStackClassVisitor maxCv = new MaxStackClassVisitor ( null ); reader.accept ( maxCv, ClassReader.SKIP_DEBUG ); ClassWriter writer = new ClassWriter ( ClassWriter.COMPUTE_FRAMES ); ClassVisitor visitor = new CallStackClassVisitor ( writer, maxCv.frameMap, className ); reader.accept ( visitor, ClassReader.SKIP_DEBUG ); result = writer.toByteArray (); return result; } } 

变换器驱动两个单独的变换 – 一个用于计算每种方法的最大堆栈帧大小,另一个用于记录记录方法。 它可能在一次通过中可行,但我不想使用ASM树API或花更多时间来计算它。

 public class MaxStackClassVisitor extends ClassVisitor { Map frameMap = new HashMap (); public MaxStackClassVisitor ( ClassVisitor v ) { super ( Opcodes.ASM4, v ); } @Override public MethodVisitor visitMethod ( int access, String name, String desc, String signature, String[] exceptions ) { return new MaxStackMethodVisitor ( super.visitMethod ( access, name, desc, signature, exceptions ), this, ( access + name + desc + signature ) ); } } public class MaxStackMethodVisitor extends MethodVisitor { final MaxStackClassVisitor cv; final String name; public MaxStackMethodVisitor ( MethodVisitor mv, MaxStackClassVisitor cv, String name ) { super ( Opcodes.ASM4, mv ); this.cv = cv; this.name = name; } @Override public void visitMaxs ( int maxStack, int maxLocals ) { cv.frameMap.put ( name, ( maxStack + maxLocals ) * 4 ); super.visitMaxs ( maxStack, maxLocals ); } } 

MaxStack * Visitor类处理最大堆栈帧大小。

 public class CallStackClassVisitor extends ClassVisitor { final Map frameSizes; final String className; public CallStackClassVisitor ( ClassVisitor v, Map frameSizes, String className ) { super ( Opcodes.ASM4, v ); this.frameSizes = frameSizes; this.className = className; } @Override public MethodVisitor visitMethod ( int access, String name, String desc, String signature, String[] exceptions ) { MethodVisitor m = super.visitMethod ( access, name, desc, signature, exceptions ); return new CallStackMethodVisitor ( m, frameSizes.get ( access + name + desc + signature ) ); } } public class CallStackMethodVisitor extends MethodVisitor { final int size; public CallStackMethodVisitor ( MethodVisitor mv, int size ) { super ( Opcodes.ASM4, mv ); this.size = size; } @Override public void visitCode () { visitIntInsn ( Opcodes.SIPUSH, size ); visitMethodInsn ( Opcodes.INVOKESTATIC, "phil/agent/MaxStackLog", "enter", "(I)V" ); super.visitCode (); } @Override 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: visitIntInsn ( Opcodes.SIPUSH, size ); visitMethodInsn ( Opcodes.INVOKESTATIC, "phil/agent/MaxStackLog", "exit", "(I)V" ); break; default: break; } super.visitInsn ( inst ); } } 

CallStack * Visitor类使用代码处理检测方法以调用堆栈帧日志记录。

然后你需要一个MANIFEST.MF用于Agent.jar:

 Manifest-Version: 1.0 Premain-Class: phil.agent.Agent Boot-Class-Path: asm-all-4.0.jar Can-Retransform-Classes: true 

最后,将以下内容添加到要执行的程序的java命令行中:

 -javaagent:path/to/Agent.jar 

您还需要将asm-all-4.0.jar与Agent.jar放在同一目录中(或更改清单中的Boot-Class-Path以引用该位置)。

示例输出可能是:

 Max stack frame size accummulated: 44140 Max stack depth: 1004 

这有点粗糙,但对我来说是有用的。

注意:堆栈帧大小不是总堆栈大小(仍然不知道如何获得那个)。 实际上,线程堆栈有各种开销。 我发现我通常需要报告的堆栈最大帧大小的2到3倍作为-Xss值。 哦,并确保在没有加载代理的情况下进行-Xss调整,因为它会增加您的堆栈大小要求。

我会减少测试环境中的-Xss设置,直到看到问题为止。 然后添加一些头部空间。

减小堆大小将为应用程序提供更多的线程堆栈空间。

只需切换到64位操作系统就可以为应用程序提供更多内存,因为大多数32位操作系统只允许每个应用程序大约1.5 GB,但64位操作系统上的32位应用程序最多可以使用3-3.5 GB在操作系统上。

Java VM中没有可用的工具来查询堆栈深度(以字节为单位)。 但你可以到达那里。 以下是一些指示:

  • exception包含堆栈帧数组,它为您提供被调用的方法。

  • 对于每种方法,您都可以在.class文件中找到Code属性 。 此属性包含字段max_stack每个方法的帧大小。

所以你需要的是一个编译HashMap的工具,它包含方法名+文件名+行号作为键,值max_stack作为值。 创建一个Throwable ,使用getStackTrace()从中获取堆栈帧,然后迭代StackTraceElement

注意:

操作数堆栈上的每个条目都可以包含任何Java虚拟机类型的值,包括long类型或double类型的值。

因此每个堆栈条目可能是64位,因此您需要将max_stack乘以8以获取字节。