gpt4 book ai didi

java - 如何测量线程堆栈深度?

转载 作者:塔克拉玛干 更新时间:2023-11-03 03:13:30 26 4
gpt4 key购买 nike

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

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

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

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

    更新 2 :我在一个专门关于 JProfiler 的相关问题上得到了很好的回答: Can JProfiler measure stack depth? (我根据 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<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(长/双占用操作数堆栈/本地变量上的两个条目,并在 max_stack 和 max_locals 中考虑)。

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

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

    另一个注意事项:我不知道 JIT/OSR 对帧大小或堆栈要求有何影响,因此请注意,-Xss 调整对冷 JVM 和暖 JVM 的影响可能不同。

    编辑 有几个小时的停机时间,并结合了另一种方法。这是一个 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仪器 Hook 。在那个钩子(Hook)中,它添加了一个类转换器,用于跟踪堆栈帧大小。它还将代理添加到引导类加载器,以便它也可以处理 jdk 类。为此,我们需要重新转换可能已经加载的任何内容,例如 String.class。但是,我们必须排除代理或堆栈日志记录使用的各种导致无限循环或其他问题的东西(其中一些是通过反复试验发现的)。最后,代理添加一个关闭 Hook 以将结果转储到标准输出。
    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 相同的目录中(或更改 list 中的 Boot-Class-Path 以引用该位置)。

    示例输出可能是:
    Max stack frame size accummulated: 44140
    Max stack depth: 1004

    这有点粗糙,但对我有用。

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

    关于java - 如何测量线程堆栈深度?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8328679/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com