1.困扰

每一个JEP都会通过Motivation描述改进的目标,通过Summary描述改进的内容,通过Description描述改进的细节与效果。

但,对于StackWalker这类新增、修改API的JEP,往往看完以后会有一种"眼睛会了手不会"的感觉(阅读它们的javadoc,也有同样的感觉)。

image-20210911112706063

以JEP259和StackWalker的javadoc为例:

摘自 JEP259-Motivation章节

There is no standard API to traverse selected frames on the execution stack efficiently and access the Class instance of each frame.

There are existing APIs that provide access to a thread’s stack:

  • Throwable::getStackTrace and Thread::getStackTrace return an array of StackTraceElement objects, which contain the class name and method name of each stack-trace element.
  • SecurityManager::getClassContext is a protected method, which allows a SecurityManager subclass to access the class context.

………………

摘自JEP259:Summary

Define an efficient standard API for stack walking that allows easy filtering of, and lazy access to, the information in stack traces.

………………

摘自javadoc:

A stack walker.

The walk method opens a sequential stream of StackFrames for the current thread and then applies the given function to walk the StackFrame stream.

The stream reports stack frame elements in order, from the top most frame that represents the execution point at which the stack was generated to the bottom most frame.

The StackFrame stream is closed when the walk method returns. If an attempt is made to reuse the closed stream, IllegalStateException will be thrown.

大意就是说,如果Java程序猿想获得虚拟机栈,用老的API(Throwable::getStackTrace),即不易用,也不高性能。新的API(StackWalker)已经完美的解决了这个问题。

阅读完JEP,笔者情不自禁地产生了若干疑问:

  • 为什么需要获得虚拟机栈?
    • 前提:我们已经理解了虚拟机栈的相关基础知识。
  • 为啥返回了Stream就要抛IllegalStateException
  • 过滤就能提升性能?——假设被搜索的全集是线性的,那么获得子集和获得全集的性能损耗似乎只有对象的创建。
  • 什么是lazy access
  • ……

这可能就是产生"眼睛会了手不会"困扰的根源:

  • JEP和javadoc默认我们已经具备了这个API相关的先验知识。
    • 比如:StackWalker的JEP已经默认我们理解了虚拟机栈、虚拟机栈帧等。
    • 比如:StackWalker的Javadoc默认我们理解JIT可能进行的栈上优化的影响。
  • JEP和javadoc无法在有限的篇幅展开技术细节,只能描述技术结论。
    • 比如:StackWalker的JEP只能描述它的性能优于老的API,却无法展开阐述它是开展的性能优化,我们不了解完整的性能优化逻辑链,就有可能错误地使用StackWalker的API(甚至,大名鼎鼎的log4j团队也在StackWalker踩过坑,见后文)。

2.基础知识回顾:虚拟机栈

  • 在JVM的运行时数据区中,有一块区域叫做"虚拟机栈”
  • JVM为每个线程维护一个虚拟机栈。
  • 每个虚拟机栈中包含若干栈帧。
  • 每个栈帧对应一个方法
    • 比如:线程1从函数1开始,函数1调用函数2,那么JVM会为线程1开辟1个虚拟机栈,函数1栈帧和函数2栈帧依次入栈,函数2执行完出栈,函数1继续执行完成后出栈。
  • 每个栈帧的最关键信息是操作数栈,每个操作数栈存储的就是这个函数的实现。
    • 操作数栈存储的内容是JVM指令序列,JVM就是在执行这个指令序列来执行这个函数体。

image-20210911115656192

3.为什么要获得虚拟机栈?

根据1.基础知识回顾:虚拟机栈,我们知道虚拟机栈可以反应某个线程内,函数的调用链,比如:

  • 场景1:业务代码从main函数开始,层层调用了哪些函数?
  • 场景2:业务代码启动了某个线程,这个线程内部层层调用了哪些函数?

什么时候需要获得"函数调用链"呢?比如:

  • 日志组件log4j,它打印错误日志的时候,就需要打印出"函数调用链”。
  • 安全检查的时候,我们可以通过"函数调用链"识别是否有不安全的调用者、不安全的调用链路。

4.How:能用

StackWalker提供了4种方法:

  • getInstace:获得StackWalker实例
  • forEach:遍历栈帧
  • walk:通过Stream方式遍历栈帧
  • 其它:getCallerClass()

image-20210911162315472

从实战角度,有两种使用方式:

  • getInstance + forEach
  • getInstance + walk

4.1.getInstance + forEach

先看一下getInstance和forEach结合使用的实例代码:

1
2
3
// getInstance + forEach
StackWalker stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
stackWalker.forEach(System.out::println);

forEach方法等效于

1
2
3
4
stackWalker.walk((s) -> {
    s.forEach(System.out::println);
    return null;
});

4.2.getInstance + walk

再看一下getInstance和walk结合使用的实例代码:

1
2
3
4
// getInstance + walk
StackWalker stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
List<StackWalker.StackFrame> stack = stackWalker.walk((s) -> s.collect(Collectors.toList()));
System.out.println(stack.toString());

通常,面向谷歌编程的我们,都能对StackWalker了解到这个程度。但想在实战中用好它,还需要进一步探索。

5.How:用好

5.1.安全性-walk()为什么禁止返回Stream?

image-20210911164640764

如上图所示,walk方法的入参function,这个回调的输入是Stream<StackFrame>,返回值是T。walk方法的返回值也是T。

从语法上,显然可以通过functionStream<StackFrame>作为walk方法的返回值保存下来。我们可以对保存下来的Stream继续进行二次操作。但运行结果是抛出了异常IllegalStateException

1
2
3
4
// 返回了Stream
Stream<StackWalker.StackFrame> returnStreamError = stackWalker.walk(frames -> frames);
// 继续对Stream进行操作
System.out.println(returnStreamError.count());

我们很容易这样想:当我们在某个时刻调用walk方法时,walk方法通过调用JVM底层某个API获得此时此刻的调用栈的快照,那么我们将这个调用栈快照以Stream的形式保存下来进行二次操作,似乎是逻辑合理的。为什么这个API要禁止这种行为呢?

然而,逻辑合理不代表安全:基于JVM在运行时的栈优化原理,JVM可能出于性能优化的理由,在任意时刻改变当前的栈结构进行修改。因此,不仅我们保存Stream的行为不安全,每次调用StackWalker#walk()方法时,都要重新调用JVM侧的native方法,重新对本次调用时刻的虚拟机栈进行快照:

image-20210911174312200

说明:上述截图,来自于StackStreamFactory.java#callStackWalk方法

5.2.易用性-getCallerClass()-简化get方式

在实战中,我们会有这样一种需求:获得调用者的Class对象

在Java9之前,我们除了通过反射法,还有一种"曲线救国"的手段:

  • STEP1.继承SecurityMananger,提供一个SecurityManager::getClassContext方法的包装接口。
    • SecurityManager::getClassContext方法可以返回调用栈的Class数组。
    • SecurityManager::getClassContext方法是protected类型的方法。
    • SecurityManager::getClassContext方法的内部实现是调用了native方法——protected native Class<?>[] getClassContext()

说明:反射法就是先通过Thread::getStackTrace方法获得调用者的类标识符,再通过反射进而获得调用者的Class对象

我们通过示例代码来感受一下这种"曲线救国"的手段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 自定义SecurityManager
public static class CustomSecurityManager extends SecurityManager {
    // 暴露protected getClassContext()方法
    public Class<?>[] getClassContextWrapper() {
        return getClassContext();
    }
}
……
private static class StackWalker3 {
    private static CustomSecurityManager customSecurityManager = new CustomSecurityManager();

    public void stackWalk3() {
        // get caller class
        Class<?>[] clazzArrOld = customSecurityManager.getClassContextWrapper();
        System.out.println(clazzArrOld[2].getName());
    }
}
……

这种方法比反射法的优点就是省略了自行反射,弊端是获取Class<?>[]中第N个元素——因为这个数组表示的调用栈的size会随着调用者不同而变化,确定要获取哪个元素的索引,将会变成隐晦的"业务潜规则”。

在Java9,有了StackWalker,我们再来体验一下新的访问方式:

1
2
3
// get caller class
Class<?> clazzNew = stackWalker.getCallerClass();
System.out.println(clazzNew.getName());

这样的感觉就很"舒服"了,一行代码就可以获得调用者的Class<?>。

getCallerClass()方法的内部实现,依然是调用了walk方法。

image-20210913083651592

说明:见JDK源码-StackStreamFactory.java

5.3.性能

JEP259中强调了StackWalker提升了获取虚拟机栈的性能,归纳网上各种技术资料的观点:

  • 观点1getCallerClass()可以提升性能
  • 观点2limit、estimateDepth、skip可以提升性能
  • 观点3:延迟加载StackFrame可以提升性能

StackWalker的本质是get的过程,

如果我们抽象一下get的过程,可以包括两个步骤:

  • STEP1.get:JDK层调用JVM的native接口,获得虚拟机此时此刻的栈帧集合。
  • STEP2.返回get的结果集:JVM的native接口返回抓取的栈帧集合。

image-20210913115502150

那么,我们可以脑补一下可行的性能优化措施:

  • 约束get的范围:支持"分页查找”,限制get的行为是获取部分栈帧集合。
  • 懒加载get的结果集:JVM层用C++实现,JDK层用Java实现,获得的栈帧集合需要从C++的内存数据转换为Java的内存数据。如果高频调用StackWalker提供的接口,频繁地反序列化栈帧集合的数据结构势必造成性能瓶颈。

image-20210913121844567

基于上述推理,我们可以进一步猜测网上各种技术资料的3个观点是否有可能逻辑成立:

  • 观点1可能没有显著的性能提升getCallerClass()仅仅是对walk的包装函数,没有约束get的范围,同时结果集的数据结构也和SecurityManager.getClassContext()的不同,没有同等的可比性。因此,观点1可能是个性能提升的伪命题。
  • 观点2可能有性能提升limit、estimateDepth、skip本质是在约束get的范围,有性能提升的可能性。
  • 观点3可能有性能提升:延迟加载StackFrame可以提升性能

我们接下来从测试数据和JDK源码,逐一论证。

5.3.1.get的方式-getCallerClass

一些文章说getCallerClass()的性能会优于SecurityManager::getClassContext(),经过实测,并没有太大的差距:

笔者写了一段测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.MICROSECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class StackWalkerTest {
    @State(Scope.Benchmark)
    public static class ShareData {
        public StackWalkerTest comp1 = new StackWalkerTest();
    }

    public static class CustomSecurityManager extends SecurityManager {
        public Class<?>[] getClassContextWrapper() {
            return getClassContext();
        }
    }

    private static final CustomSecurityManager customSecurityManager = new CustomSecurityManager();

    @Benchmark
    public Object securityManager() {
        return customSecurityManager.getClassContextWrapper()[1];
    }

    @Benchmark
    public Object stackWalker() {
        return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass();
    }
}

测试结果如下:

1
2
3
Benchmark                        Mode  Cnt  Score   Error  Units
StackWalkerTest.securityManager  avgt    3  1.112 ± 1.830  us/op
StackWalkerTest.stackWalker      avgt    3  1.159 ± 2.166  us/op

从测试结果看,getCallerClass()并没有性能提升。

进一步对比JDK源码:

  • SecurityManager::getClassContext()最终调用了
1
native Class<?>[] getClassContext()
  • StackWalker::getCallerClass()最终调用了
1
native R callStackWalk(long mode, int skipframes, int batchSize, int startIndex, T[] frames)

暂时没有再深入到JVM的源码,但可以脑补一下,JVM层抓取虚拟机栈的机制不可能有极大的变化,即使做了特殊处理,也不可能有极大的性能提升。

综合上述测试结果和源码分析,我们基本可以得到这样的结论:StackWalker::getCallerClass()性能方面没有太大的变化

5.3.2.get的范围-limit、estimateDepth、skip

先看一下比较常用的对walk的Stream处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// filter1
final List<Class<?>> filter1 = new ArrayList<>();
filter1.add(StackWalker1.class);
Optional<StackWalker.StackFrame> filter1Res = stackWalker
    .walk(s -> s.filter(f -> filter1.contains(f.getDeclaringClass())).findFirst());
System.out.println(filter1Res);

// filter2
System.out.println("Filter2:");
List<StackWalker.StackFrame> filter2 = stackWalker.walk(s -> s.limit(2).collect(Collectors.toList()));
System.out.println(filter2);

写到这里,我们只能得到这样一个结论:

  • 性能提升的可能性StackWalker.walk()方法通过支持业务侧根据需要获得栈帧子集,这只是性能提升的可能性。
    • 如果业务侧需要获得栈帧的全集,那么也不能说这就是StackWalker.walk()方法性能提升的根因。

但,我们知道JDK提供的很多集合类,都存在”初始容量“问题,因此采用limit、skip方式限定get的范围可能有不同的性能表现。

5.3.2.1.用limit限定get范围

栈本质是线性结构,如果栈帧数量为N,那么理论上不断增加limit的数量M,查询时间应该是线性递增的。

笔者写了这样一段测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.MICROSECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class LimitTest {
    @Param(value = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18"})
    int limit;

    private StackWalker stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);

    @Benchmark
    public void stackWalker(Blackhole b) {
        // LimitTestObj构造了一个多级调用的调用栈
        LimitTestObj.test1(() -> {
            // 用limit处理Stream
            stackWalker.walk(s -> {
                s.limit(limit).forEach(b::consume);
                return null;
            });
        });
    }
}

附LimitTestObj代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class LimitTestObj {
    public static void test1(ILimitTestCallback callback) {
        test2(callback);
    }

    public static void test2(ILimitTestCallback callback) {
        test3(callback);
    }

    public static void test3(ILimitTestCallback callback) {
        test4(callback);
    }

    public static void test4(ILimitTestCallback callback) {
        test5(callback);
    }

    public static void test5(ILimitTestCallback callback) {
        test6(callback);
    }

    public static void test6(ILimitTestCallback callback) {
        test7(callback);
    }

    public static void test7(ILimitTestCallback callback) {
        test8(callback);
    }

    public static void test8(ILimitTestCallback callback) {
        test9(callback);
    }

    public static void test9(ILimitTestCallback callback) {
        test10(callback);
    }

    public static void test10(ILimitTestCallback callback) {
        test11(callback);
    }

    public static void test11(ILimitTestCallback callback) {
        test12(callback);
    }

    public static void test12(ILimitTestCallback callback) {
        test13(callback);
    }

    public static void test13(ILimitTestCallback callback) {
        test14(callback);
    }

    public static void test14(ILimitTestCallback callback) {
        test15(callback);
    }

    public static void test15(ILimitTestCallback callback) {
        test16(callback);
    }

    public static void test16(ILimitTestCallback callback) {
        test17(callback);
    }

    public static void test17(ILimitTestCallback callback) {
        test18(callback);
    }

    public static void test18(ILimitTestCallback callback) {
        test19(callback);
    }

    public static void test19(ILimitTestCallback callback) {
        callback.run();
    }
}

interface ILimitTestCallback {
    void run();
}

测试结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Benchmark	(limit)	Mode	Cnt	Score		Error	Units
LimitTest.stackWalker	1	avgt	3	1.297	±	5.239	us/op
LimitTest.stackWalker	2	avgt	3	1.467	±	14.593	us/op
LimitTest.stackWalker	3	avgt	3	1.964	±	2.089	us/op
LimitTest.stackWalker	4	avgt	3	2.371	±	0.93	us/op
LimitTest.stackWalker	5	avgt	3	3.147	±	1.169	us/op
LimitTest.stackWalker	6	avgt	3	3.744	±	8.11	us/op
LimitTest.stackWalker	7	avgt	3	3.808	±	5.468	us/op
LimitTest.stackWalker	8	avgt	3	7.098	±	1.072	us/op
LimitTest.stackWalker	9	avgt	3	5.94	±	3.466	us/op
LimitTest.stackWalker	10	avgt	3	6.172	±	14.064	us/op
LimitTest.stackWalker	11	avgt	3	6.202	±	3.207	us/op
LimitTest.stackWalker	12	avgt	3	6.467	±	18.106	us/op
LimitTest.stackWalker	13	avgt	3	6.714	±	4.904	us/op
LimitTest.stackWalker	14	avgt	3	6.667	±	29.945	us/op
LimitTest.stackWalker	15	avgt	3	6.767	±	7.403	us/op
LimitTest.stackWalker	16	avgt	3	8.857	±	8.56	us/op
LimitTest.stackWalker	17	avgt	3	6.682	±	7.17	us/op
LimitTest.stackWalker	18	avgt	3	6.705	±	3.477	us/op

image-20210913144346707

我们可以发现测试结果并没有完全遵循线性规律,而是在limit=8、limit=16出现一个性能跳变。

为什么会出现性能跳变呢?这里细化一下get的调用过程,如下图:

image-20210913145348392

所以,遇到8的倍数,就有可能因为Buffer的扩容出现性能跳变,打破性能的线性化增长。

我们如何消峰呢?根据StackStreamFactory的源码,初始化Buffer时,如果StackWalker.getInstace时设置了estimateDepath参数,Buffer的Size就是以此为准。

image-20210913151127327

因此,我们可以将limit和estimateDepth结合起来,减少Buffer扩容的影响,示例代码:

1
2
3
4
5
StackWalker.getInstance(Set.of(StackWalker.Option.RETAIN_CLASS_REFERENCE), limit + 2)
    .walk(s -> {
        s.limit(limit + 2).forEach(b::consume);
        return null;
    });

基于limit和estimateDepth结合的做法,测试结果中8的峰值被消减掉了:

image-20210913153246532

5.3.2.2.skip

我们编写了这样一段测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.MICROSECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class LimitTest4 {
    @Param(value = {"1", "2", "4", "6", "8", "10", "12", "14", "16"})
    int skip;

    @Benchmark
    public void stackWalker(Blackhole b) {
        LimitTestObj.test1(() -> {
            StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
                    .walk(s -> {
                        s.skip(skip).forEach(b::consume);
                        return null;
                    });
        });
    }
}

测试结果是skip并不会产生太大的性能差异:

image-20210913155815509

为什么skip并不会带来性能的差异呢?

因为,根据前文细化的get细节,即使设置了skip,StackWalker也是从第一帧开始调用JVM::fectchStackFrame方法。

image-20210913145348392

5.3.2.3.小结

至此,我们可以得到这样的结论:

  • StackWalker.walk()方法支持业务侧限定get范围,给了业务侧性能提升的可能性。
    • 在Java9之前,即使业务侧只需要获得1个栈帧,JDK也会获得JVM中的全部栈帧。
  • StackWalker.walk()方法在限定get范围时,如果可以预估栈帧的size,可以通过limit+estimateDepth降低性能跳变。

5.3.3.get结果的处理-StackFrame

在JEP259中,强调了StackTraceElement类是一个代价不菲的数据结构。

image-20210913162826236

我们来详细解读一下StackFrameStackFrameInfoStackTraceElement的关系:

首先,在walk的过程中仅仅会创建StackFrameInfo,它实现了StackFrame接口,StackFrameInfo只有简单的几个属性:

image-20210913163134017

其中,JLIAJavaLangInvokeAccess类型的,一路跟踪进去,它本质就是对java.lang.invoke包下的一组API的封装。

也就是说,在walk期间构造的StackFrameInfo仅仅通过JVM的native接口获得了一些足够支撑反射的信息,大部分在JDK侧通过反射就能获得的信息,就不用调用性能代价更高的JVM的native接口去获取了。

另外,调用了StackFrameInfo对象的getFileName()getLineNumber()toString()方法后,这些方法会调用toStackTraceElement(),这个函数将会生成StackFrameElement对象,在这个对象中,将会调用JVM的native接口,虽然StackeFrameElement具备完整的栈帧信息,但是需要通过JVM的native接口获得,所以性能将下降很多。

image-20210913164023218

我们可以这样进行对比测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.MICROSECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class ToStringTest {
    @State(Scope.Benchmark)
    public static class ShareData {
        public ToStringTest comp1 = new ToStringTest();
    }

    @Benchmark
    public void getStackTrace(Blackhole b) {
        var stackTraceElementArr = new Throwable().getStackTrace();
        for (StackTraceElement e : stackTraceElementArr) {
            b.consume(e);
        }
    }

    @Benchmark
    public void stackWalker(Blackhole b) {
        StackWalker.getInstance().forEach(f -> b.consume(f.toStackTraceElement()));
    }
}

测试结果发现:如果在不必要的场景下,触发了StackFrameElement的生成,性能表现还不如Java9之前使用Thread::getStackTrace

1
2
3
Benchmark                   Mode  Cnt   Score    Error  Units
ToStringTest.getStackTrace  avgt    3  11.047 ± 16.395  us/op
ToStringTest.stackWalker    avgt    3  20.032 ± 27.051  us/op

至此,我们可以得到两个结论:

  • StackWalker性能优化的核心原理:JDK侧通过反射可以获得虚拟机栈中的大部分信息,JVM侧中虽然有虚拟机栈的全量信息,但从JDK侧获取的性能代价非常小,因此StackWalker所谓的延迟加载就是在非必要的情况下绝不去从JVM去获取虚拟机栈的信息。
  • 慎用StackWalker的性能敏感方法getFileName()getLineNumber()toString()方法会触发从JVM获取虚拟机栈的信息,如果业务非必要,慎用。

6.思考

在学习StackWalker之初(见第4章节),是模糊的,

在理解了StackWalker的内部实现(见第5.3.1~5.3.3章节),我们形成了JDK完整的”性能提升逻辑链":

  • 性能优化的主要措施StackWalker主要是使用轻量级对象StackFrameInfo,降低调用JVM的native接口的性能消耗,这才是性能消耗的大头。
  • 性能优化的辅助措施StackWalker通过支持业务侧限定查询结果的范围,辅助降低了性能消耗。

有了完整的"性能提升逻辑链”,我们才能得到用好StackWalker的实战经验

  • 缩小get的范围:业务侧可以根据需要,通过limit+estimateDepth,提升获取虚拟机栈的性能。
  • 尽量使用轻量级结果对象:业务侧可以尽量避免调用getFileName()getLineNumber()toString()方法。

笔者在查阅StackWalker相关资料的时候,还发现了一个有趣与Log4J有关的案例:

https://issues.apache.org/jira/browse/LOG4J2-2880

Log4J有这么一个问题单,大致意思就是升级为Java11之后,Log4j会导致CPU得到100%,这个结果将会相当严重。。。

Log4J的程序猿最后定位的原因是业务侧没有使用private static final修饰logger对象,同时业务侧采用的是ZGC,

而logger对象记录日志时又调用了StackWalker,StackWalker调用JVM::fetch接口时在C++的代码中产生的栈帧对象又不会被ZGC回收(ZGC的Bug),而C++代码中的栈帧对象又是以Map的形式存储,当Map中的对象越来越多,Hash冲突就越多,于是越查找越慢,最后JVM不断消耗CPU。。。

这也是触动笔者较大的感触:学习标准库的初级境界是”能用",高级的境界是”用好",“用好"的关键有依赖于程序猿的基本功探索力

  • 基本功:面对一种编程语言,基本功往往是先验知识。
    • 不要以"实战中用不到"给自己设限(仅停留在语言语法层面),最好能深入到底层(API的源码->JVM->操作系统->硬件)。
  • 探索逻辑的完备性:不要盲目接受新特性"宣称"的优点,特别是一些性能敏感的API。通过JDK/JVM源码推理出完备的逻辑链,避免在产品中踩坑。
  • 探索细节背后的故事:不要忽略javadoc里的细节或结论,这些细节很可能是API提供者曾经花费大量精力攻克的难关,也可能是API提供者设计的精妙机关。

没有达到”用好“的境界,就有可能在不合适的场景下使用”新特性/新API",最终得到极差的**“性能”、“安全性”**等。

7.参考

https://issues.apache.org/jira/browse/LOG4J2-2880

https://mail.openjdk.java.net/pipermail/zgc-dev/2019-March/000612.html

https://openjdk.java.net/jeps/259

https://cr.openjdk.java.net/~mchung/jdk9/jep259/api/java/lang/StackWalker.html