【问题标题】:How can I measure thread stack depth?如何测量线程堆栈深度?
【发布时间】:2012-01-09 20:24:31
【问题描述】:

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

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

  1. 在集成测试期间测量正在运行的 JVM。哪些分析工具会报告最大堆栈深度?
  2. 寻找深层调用层次结构的应用程序的静态分析。依赖注入中的反射使得这不太可能起作用。

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

更新 2:我在一个专门关于 JProfiler 的相关问题上得到了一个很好的答案:Can JProfiler measure stack depth?(我根据 JProfiler 的社区支持建议发布了单独的问题)

【问题讨论】:

  • 您应该考虑切换到异步模型。拥有比系统中的 CPU 内核更多的线程没有任何意义。
  • @VladLazarenko - 同意。正如我所说,从长远来看,我计划降低每用户的线程比率,但我需要尽快快速修复。
  • 您创建了多少线程以及如何管理它们?为什么你需要一个线程每个用户,你不能重用线程并提供线程每个请求?
  • @JohnVint - 是的,我已经计划修复这个遗留代码以使用 NIO 而不是每个套接字的线程,但这在短时间内是不可行的。这个应用程序不是请求/响应,但有消息以不可预测的时间间隔通过已建立的套接字到达。
  • @Vlad 你可能有兴趣阅读this

标签: java multithreading profiling


【解决方案1】:

您可以通过可以编织到代码中的方面(加载时间编织器,允许建议除系统类加载器之外的所有加载代码)来了解堆栈深度。该方面将围绕所有已执行的代码工作,并且能够记录您何时调用方法以及何时返回。您可以使用它来捕获大部分堆栈使用情况(您会错过从系统类加载器加载的任何内容,例如 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<ArrayDeque<String>> curStack = 
        new ThreadLocal<ArrayDeque<String>> () {
        @Override
        protected ArrayDeque<String> initialValue () {
            return new ArrayDeque<String> ();
        }
    };

    private static ThreadLocal<Boolean> ascending = 
        new ThreadLocal<Boolean> () {
        @Override
        protected Boolean initialValue () {
            return true;
        }
    };

    private static ConcurrentHashMap<Integer, ArrayDeque<String>> stacks = 
         new ConcurrentHashMap<Integer, ArrayDeque<String>> ();

    public static void calling ( String signature ) {
        ascending.set ( true );
        curStack.get ().push ( signature.intern () );
    }

    public static void exiting ( String signature ) {
        ArrayDeque<String> cur = curStack.get ();
        if ( ascending.get () ) {
            ArrayDeque<String> clon = cur.clone ();
            stacks.put ( hash ( clon ), clon );
        }
        cur.pop ();
        ascending.set ( false );
    }

    public static Integer hash ( ArrayDeque<String> 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 来计算该方法的帧大小(请参阅class file format)。基于vm spec,我认为方法的最大帧大小应该是 (max_stack+max_locals)*4bytes(long/double 占用操作数堆栈/本地变量上的两个条目,并在 max_stack 和 max_locals 中进行说明)。

如果您的调用堆栈中没有那么多,您可以轻松地 javap 感兴趣的类并查看帧值。 asm 之类的内容为您提供了一些简单的工具,可用于更大规模地执行此操作。

计算完此值后,您需要估计 JDK 类的额外堆栈帧,这些堆栈帧可能会在您的最大堆栈点处被您调用,并将其添加到您的堆栈大小中。它不会是完美的,但它应该为您提供一个不错的 -Xss 调优起点,而无需围绕 JVM/JDK 进行黑客攻击。

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

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

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

package phil.agent;

public class MaxStackLog {

    private static ThreadLocal<Integer> curStackSize = 
        new ThreadLocal<Integer> () {
        @Override
        protected Integer initialValue () {
            return 0;
        }
    };

    private static ThreadLocal<Integer> curStackDepth = 
        new ThreadLocal<Integer> () {
        @Override
        protected Integer initialValue () {
            return 0;
        }
    };

    private static ThreadLocal<Boolean> ascending = 
        new ThreadLocal<Boolean> () {
        @Override
        protected Boolean initialValue () {
            return true;
        }
    };

    private static ConcurrentHashMap<Long, Integer> maxSizes = 
        new ConcurrentHashMap<Long, Integer> ();
    private static ConcurrentHashMap<Long, Integer> maxDepth = 
        new ConcurrentHashMap<Long, Integer> ();

    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 ();
            }
        } );
    }
}

代理类具有用于检测的premain 挂钩。在那个钩子中,它添加了一个类转换器,用于在堆栈帧大小跟踪中进行检测。它还将代理添加到引导类加载器,以便它也可以处理 jdk 类。为此,我们需要重新转换任何可能已经加载的内容,例如 String.class。但是,我们必须排除代理使用的各种东西或导致无限循环或其他问题的堆栈日志记录(其中一些是通过反复试验发现的)。最后,代理添加了一个关闭挂钩以将结果转储到标准输出。

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<String, Integer> frameMap = new HashMap<String, Integer> ();

    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<String, Integer> frameSizes;
    final String className;

    public CallStackClassVisitor ( ClassVisitor v, 
        Map<String, Integer> 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 类使用代码处理检测方法以调用堆栈帧日志记录。

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

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 调整,因为它会增加您的堆栈大小要求。

【讨论】:

  • 我在您的方法中看到的唯一弱点是,当从堆栈下方的方法抛出异常时,它无法处理。这需要一个 try/finally 块。
【解决方案2】:

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

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

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

【讨论】:

  • 是的,我们已经在尝试这种方法,但现在测试只是二进制:我们是否得到 StackOverflowError ?我想更详细地了解应用程序的实际堆栈使用情况。关于堆大小的好点,我忘记了。是的,64 位在长期计划中,但该应用还有剩余的 32 位本机依赖项需要工作。
  • +1 尽管这是一种反复试验的方法,但它可以完成工作。太糟糕了jvisualvm 不提供这样的信息。
  • 即使您有 32 位本机代码,但即使使用 32 位 JVM,64 位操作系统也会为您提供更多内存。
  • @PeterLawrey - 哎呀,我误读了您的“64 位操作系统”观点。是的,我们已经在 64 位操作系统上运行,并且正在利用额外的 RAM。
  • 您能否将您的 32 位本机库移动到一个独立的 JVM,该 JVM 可以作为服务提供给您的前端。即客户端连接到没有 32 位库的 64 位 JVM 服务器,该服务器又连接到保存您的本机库的 JVM。
【解决方案3】:

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

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

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

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

注意:

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

所以每个堆栈条目大概是 64 位,所以需要将 max_stack 乘以 8 得到字节。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-02
    • 1970-01-01
    • 1970-01-01
    • 2022-10-05
    • 1970-01-01
    • 2011-07-20
    相关资源
    最近更新 更多