2011-11-30 71 views
9

我有一個可伸縮性問題的32位Java服務:由於線程數過多,用戶數很高,我們的內存不足。從長遠來看,我計劃切換到64位並降低每用戶線程數。在短期內,我想減少堆棧大小(-Xss,-XX:ThreadStackSize)以獲得更多的空間。但這是有風險的,因爲如果我使它太小,我會得到StackOverflowErrors。如何測量線程堆棧深度?

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

  1. 在集成測試期間測量正在運行的JVM。哪些性能分析工具會報告最大堆棧深度?
  2. 尋找深度調用層次結構的應用程序的靜態分析。依賴注入中的反射使得這不太可能起作用。

更新:我知道來解決這個問題的長期正確的方式。請關注我所問的問題:我如何衡量堆棧深度?

更新2:我得到了一個相關的問題一個很好的答案特別是約的JProfiler:Can JProfiler measure stack depth?(我張貼的另外一個問題是每JProfiler的社區支持的建議)

+1

您應該考慮切換到異步模式。在系統中擁有比CPU核更多的線程沒有任何意義。 – 2011-11-30 15:47:37

+2

@ VladLazarenko - 同意。正如我所說,從長遠來看,我打算減少每用戶線程數,但我需要儘快解決。 –

+0

您創建了多少個線程?您如何管理它們?爲什麼你需要一個每用戶線程,你不能重用線程,並提供線程每請求? –

回答

6

你可以得到類似的東西可被編織到您的代碼(加載時織允許建議除了系統類加載器加載的所有代碼)一個方面的堆棧深度的想法。這個方面可以解決所有執行的代碼,並且能夠記錄你何時調用方法以及何時返回。您可以使用它來捕獲大部分堆棧使用情況(您將錯過從系統類加載器加載的任何內容,例如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)*的最大幀大小的方法(長/雙佔據了操作數棧/本地變量兩個條目,並佔max_stack和max_locals)4字節。

如果您的調用堆棧中沒有那麼多,您可以輕鬆javap感興趣的類並查看幀值。而像asm這樣的東西爲您提供了一些簡單的工具,可以用於更大規模地實現此目的。

一旦你有了這個計算,你需要估計更多的堆棧幀的JDK類,可能會爲你在你的最大堆棧點被調用,並添加到您的堆棧大小。它不會是完美的,但它應該讓你在沒有黑客入侵JVM/JDK的情況下進行-Xss調整。其他

一注:我不知道是什麼JIT/OSR確實給幀大小或堆棧需求所以千萬要注意,你可能有從-Xss調整在一個寒冷的溫暖與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鉤儀器。在該鉤子中,它添加了一個類變換器,用於在堆棧幀大小跟蹤中進行測量。它還將代理添加到引導類加載器,以便它也可以處理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); 
    } 
} 

調用堆棧*訪客類處理插裝用代碼的方法調用堆棧幀記錄。

然後你需要爲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調優,因爲它會增加您的堆棧大小要求。

+0

我可以在你的方法中看到的唯一弱點是,當一個異常從堆棧下方的方法拋出時,它不會處理。這將需要try/finally塊。 – mchr

5

我將減少在測試環境中-Xss設置直到你看到一個問題。然後添加一些頭部空間。

減少堆大小,會給您的應用程序更多的空間用於線程堆棧。

只需切換到64位操作系統可以給你的應用程序更多的內存,因爲大多數32位操作系統只允許約1.5 GB爲每個應用程序,然而,在64位操作系統的32位應用程序最多可以使用3 -3.5 GB取決於操作系統。

+0

是的,我們已經在嘗試這種方法,但現在測試只是二進制的:我們是否得到StackOverflowError?我想更詳細地瞭解應用程序的實際堆棧使用情況。關於堆大小的好處,我忘記了這一點。是的,64位計劃是長期計劃的,但該應用程序仍有32位本地依賴需要工作。 –

+2

+1雖然這是一種試錯方法,但它可以完成工作。這太糟糕了'jvisualvm'不提供這樣的信息。 –

+1

即使您擁有32位本機代碼,即使使用32位JVM,64位操作系統也會爲您提供更多內存。 –

3

有在Java VM沒有現成可用工具來查詢字節堆棧深度。但你可以到達那裏。這裏有一些指針:

  • 異常包含堆棧幀的數組,它給你被調用的方法。

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

所以,你需要的是一個工具,編譯一個HashMap包含方法名+文件名+行號鍵和值值max_stack。創建Throwable,與getStackTrace()從它取回堆棧幀,然後遍歷StackTraceElement

注:

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

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