簡體   English   中英

如何測量線程堆棧深度?

[英]How can I measure thread stack depth?

我有一個帶有可伸縮性問題的32位Java服務:由於線程計數過多,用戶數量很多,因此內存不足。 從長遠來看,我計划切換到64位並減少每用戶線程數。 在短期內,我想減少堆棧大小(-Xss,-XX:ThreadStackSize)以獲得更多的空間。 但這有風險,因為如果我把它做得太小,我就會得到StackOverflowErrors。

如何衡量應用程序的平均和最大堆棧大小,以指導我決定最佳-Xss值? 我對兩種可能的方法感興趣:

  1. 在集成測試期間測量正在運行的JVM。 什么分析工具將報告最大堆棧深度?
  2. 尋找深度調用層次結構的應用程序的靜態分析。 依賴注入的反思使得這不可行。

更新 :我知道解決此問題的長期正確方法。 請關注我問過的問題:如何測量堆棧深度?

更新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<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來計算方法的幀大小(請參閱類文件格式 )。 根據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<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。 但是,我們必須排除代理程序使用的各種內容或堆棧日志記錄導致無限循環或其他問題(其中一些是通過反復試驗找到的)。 最后,代理添加了一個關閉鈎子以將結果轉儲到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<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類使用代碼處理檢測方法以調用堆棧幀日志記錄。

然后你需要一個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中沒有可用的工具來查詢堆棧深度(以字節為單位)。 但是你可以到達那里。 以下是一些指示:

  • 異常包含堆棧幀數組,它為您提供被調用的方法。

  • 對於每種方法,您都可以在.class文件中找到Code屬性 此屬性包含字段max_stack每個方法的幀大小。

所以你需要的是一個編譯HashMap的工具,它包含方法名+文件名+行號作為鍵,值max_stack作為值。 創建一個Throwable ,使用getStackTrace()從中獲取堆棧幀,然后迭代StackTraceElement

注意:

操作數堆棧上的每個條目都可以包含任何Java虛擬機類型的值,包括long類型或double類型的值。

因此每個堆棧條目可能是64位,因此您需要將max_stack與8相乘以獲取字節。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM