Как я могу измерить глубину стека потока? - PullRequest
9 голосов
/ 30 ноября 2011

У меня 32-битный Java-сервис с проблемами масштабируемости: при большом количестве пользователей у нас заканчивается память из-за чрезмерного количества потоков.В долгосрочной перспективе я планирую перейти на 64-разрядную версию и сократить соотношение потоков на пользователя.В краткосрочной перспективе я бы хотел уменьшить размер стека (-Xss, -XX: ThreadStackSize), чтобы получить больше запаса.Но это рискованно, потому что, если я сделаю его слишком маленьким, я получу StackOverflowErrors.

Как я могу измерить средний и максимальный размер стека для моего приложения, чтобы принять оптимальное решение -Значение Xss? Меня интересуют два возможных подхода:

  1. Измерение работающей JVM во время интеграционного тестирования.Какие инструменты профилирования сообщат о максимальной глубине стека?
  2. Статический анализ приложения, ищущего глубокие иерархии вызовов.Отражение в внедрении зависимостей делает маловероятным, что это сработает.

Обновление : я знаю долгосрочный правильный способ решения этой проблемы.Пожалуйста, обратите внимание на вопрос, который я задавал: как мне измерить глубину стека?

Обновление 2 : я получил хороший ответ на связанный вопрос, конкретно о JProfiler: Может ли JProfiler измерить глубину стека? (я разместил отдельный вопрос в соответствии с рекомендациями поддержки JProfiler)

Ответы [ 3 ]

6 голосов
/ 01 декабря 2011

Вы можете получить представление о глубине стека с помощью чего-то вроде аспекта, который может быть вплетен в ваш код (ткач времени загрузки, позволяющий посоветовать весь загруженный код, кроме загрузчика системных классов). Аспект будет работать вокруг всего исполняемого кода и сможет заметить, когда вы вызываете метод и когда вы возвращаетесь. Вы можете использовать это для захвата большей части использования стека (вы пропустите все, что загружено из системного загрузчика классов, например, 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 spec Я полагаю, что это должно быть (max_stack + max_locals) * 4 байта для максимального размера кадра для метода (long / double занимают две записи в стеке операндов / локальных переменных и учитываются в max_stack и max_locals).

Вы можете легко создавать интересующие вас классы и просматривать значения фреймов, если в стеках вызовов их не так много. И что-то вроде asm предоставляет вам несколько простых инструментов для более масштабного использования.

Как только вы это вычислите, вам нужно оценить дополнительные кадры стека для классов JDK, которые вы можете вызывать в ваших точках максимального стека, и добавить это к размеру стека. Он не будет идеальным, но он должен дать вам достойную отправную точку для настройки -Xss без взлома JVM / JDK.

Еще одно замечание: я не знаю, что JIT / OSR делает с размерами кадра или требованиями к стеку, поэтому имейте в виду, что вы можете иметь различные последствия от настройки -Xss на холодной и теплой JVM.

EDIT провел несколько часов простоя и собрал другой подход. Это Java-агент, который будет использовать методы для отслеживания максимального размера кадра стека и глубины стека. Это позволит использовать большинство классов jdk вместе с другим вашим кодом и библиотеками, что даст вам лучшие результаты, чем у сторонника ткачества. Вам нужно ASM v4 для этого, чтобы работать. Это было больше для удовольствия, так что подайте это под пловцом 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;
    }
}

Преобразователь запускает два отдельных преобразования - одно для определения максимального размера кадра стека для каждого метода, а другое - для метода записи. Это может быть выполнимо за один проход, но я не хотел использовать API дерева ASM или тратить больше времени на его выяснение.

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 в манифесте для ссылки на местоположение).

AПример вывода может быть следующим:

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

Это все немного грубовато, но я могу начать.

Примечание: размер кадра стека не является общим размером стекадействительно не знаю, как получить это).На практике существует множество накладных расходов для стека потоков.Я обнаружил, что обычно мне нужно в 2-3 раза превышать максимальный размер кадра стека, указанный в качестве значения -Xss.Да, и обязательно выполняйте настройку -Xss без загруженного агента, поскольку это увеличивает ваши требования к размеру стека.

5 голосов
/ 30 ноября 2011

Я бы уменьшил настройку -Xss в тестовой среде, пока вы не увидите проблему. Затем добавьте голову.

Уменьшение размера кучи даст вашему приложению больше места для стеков потоков.

Простое переключение на 64-битную ОС может дать вашему приложению больше памяти, так как большинство 32-битных ОС допускают только около 1,5 ГБ для каждого приложения, однако 32-битное приложение в 64-битной ОС может использовать до 3- 3,5 ГБ в зависимости от ОС.

3 голосов
/ 01 декабря 2011

В Java VM нет удобного инструмента для запроса глубины стека в байтах. Но вы можете туда добраться. Вот несколько указателей:

  • Исключения содержат массивы стековых фреймов, которые дают вам методы, которые были вызваны.

  • Для каждого метода вы можете найти атрибут Code в файле .class. Этот атрибут содержит размер кадра для каждого метода в поле max_stack.

Итак, вам нужен инструмент, который компилирует HashMap, который содержит имя метода + имя файла + номер строки в качестве ключей и значение max_stack в качестве значений. Создайте Throwable, извлеките из него кадры стека с помощью getStackTrace() и затем выполните итерации по StackTraceElement s.

Примечание:

Каждая запись в стеке операндов может содержать значение любого типа виртуальной машины Java, включая значение типа long или типа double.

Таким образом, каждая запись в стеке, вероятно, составляет 64 бита, поэтому вам нужно умножить max_stack на 8, чтобы получить байты.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...