.class.getClassLoader()频繁调用是否有较大性能开销?

前言

今天工作中与同事就 classLoader 获取方式的性能差异展开了争论。

问题概括为:在一个频繁被调用的方法中,通过本地 static 变量获取 classLoader 和通过.class.getClassLoader 到底哪种方式更加性能优越,开销更少?

测试环境

环境项 环境信息
操作系统 macOS Catalina 10.15.4
处理器 2.4 GHz 八核 Intel Core i9
内存 64 GB 2667 MHz DDR4
Java Version openjdk version “1.8.0_282”

OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_282-b08)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.282-b08, mixed mode) |

验证

测试类准备

下面我写了一个测试类来验证两种获取 classloader 方式的开销差异。为了避免一些 cache 影响以及运行时的差异,两个 testcase 分成 2 个独立的进程分别运行,统计时间开销。

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
package com.kaimingwan.questions;

import com.google.common.base.Stopwatch;
import com.kaimingwan.questions.model.Student;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

/**
* @author wanshao create time is 2022/2/23
**/
@Slf4j
public class StaticFieldPerfTest {

private static ClassLoader staticStuClzLoader = Student.class.getClassLoader();

@Test
public void useStaticClassLoad() {
int count = 1000000;
Stopwatch stopwatch = Stopwatch.createStarted();
// 1. Get class loader from static field
for (int i = 0; i < count; i++) {
getClzLoaderByStatic();
}
stopwatch.stop();

log.info("Get class loader from static cost "+stopwatch.elapsed(TimeUnit.NANOSECONDS)+" ns.");

}

@Test
public void useNativeClassLoad() {
int count = 1000000;
Stopwatch stopwatch = Stopwatch.createStarted();
// 1. Get class loader from static field
for (int i = 0; i < count; i++) {
getClzLoaderByStatic();
}
stopwatch.stop();

log.info("Get class loader from native cost "+stopwatch.elapsed(TimeUnit.NANOSECONDS)+" ns.");

}

private ClassLoader getClzLoaderByStatic() {
return staticStuClzLoader;
}

private ClassLoader getClzLoaderByNativeKeyword() {
return Student.class.getClassLoader();
}
}

测试结果

Static 访问方式

测试循环次数 耗时
1000 万 平均 300 万~400 万纳秒
1 亿 平均 300 万~400 万纳秒
10 亿 平均 300 万~400 万纳秒

Native 访问方式

测试循环次数 耗时
1000 万 平均 300 万~400 万纳秒
1 亿 平均 300 万~400 万纳秒
10 亿 平均 300 万~400 万纳秒

单纯从测试结果来看两种方式差异很小,而且循环次数对其实际的开销影响非常小。

汇编指令查看

通过反编译代码我们看下具体的汇编指令,了解两种方式的底层差异。

代码上做了一些调整,使得汇编内容更加精简。下面展示对应的源码和字节码。使用 IDEA 的 jclasslib 查看字节码汇编信息。
NativeLoad

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kaimingwan.questions;


import com.kaimingwan.questions.model.Student;

/**
* @author wanshao create time is 2022/2/23
**/
public class NativeLoad {

public static void main(String[] args) {
NativeLoad nativeLoad = new NativeLoad();
for (int i = 0; i < 100; i++) {
nativeLoad.useNativeClassLoad();
}
}

public void useNativeClassLoad() {
ClassLoader clzLoader = Student.class.getClassLoader();
}
}

字节码

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
Classfile /Users/wanshao/projects/java-questions/target/classes/com/kaimingwan/questions/NativeLoad.class
Last modified 2022-2-25; size 814 bytes
MD5 checksum 15dd15338ee0440807c7742e76473e7f
Compiled from "NativeLoad.java"
public class com.kaimingwan.questions.NativeLoad
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#29 // java/lang/Object."<init>":()V
#2 = Class #30 // com/kaimingwan/questions/NativeLoad
#3 = Methodref #2.#29 // com/kaimingwan/questions/NativeLoad."<init>":()V
#4 = Methodref #2.#31 // com/kaimingwan/questions/NativeLoad.useNativeClassLoad:()V
#5 = Class #32 // com/kaimingwan/questions/model/Student
#6 = Methodref #33.#34 // java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;
#7 = Class #35 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/kaimingwan/questions/NativeLoad;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 i
#18 = Utf8 I
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 nativeLoad
#22 = Utf8 StackMapTable
#23 = Class #30 // com/kaimingwan/questions/NativeLoad
#24 = Utf8 useNativeClassLoad
#25 = Utf8 clzLoader
#26 = Utf8 Ljava/lang/ClassLoader;
#27 = Utf8 SourceFile
#28 = Utf8 NativeLoad.java
#29 = NameAndType #8:#9 // "<init>":()V
#30 = Utf8 com/kaimingwan/questions/NativeLoad
#31 = NameAndType #24:#9 // useNativeClassLoad:()V
#32 = Utf8 com/kaimingwan/questions/model/Student
#33 = Class #36 // java/lang/Class
#34 = NameAndType #37:#38 // getClassLoader:()Ljava/lang/ClassLoader;
#35 = Utf8 java/lang/Object
#36 = Utf8 java/lang/Class
#37 = Utf8 getClassLoader
#38 = Utf8 ()Ljava/lang/ClassLoader;
{
public com.kaimingwan.questions.NativeLoad();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/kaimingwan/questions/NativeLoad;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/kaimingwan/questions/NativeLoad
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: bipush 100
13: if_icmpge 26
16: aload_1
17: invokevirtual #4 // Method useNativeClassLoad:()V
20: iinc 2, 1
23: goto 10
26: return
LineNumberTable:
line 12: 0
line 13: 8
line 14: 16
line 13: 20
line 16: 26
LocalVariableTable:
Start Length Slot Name Signature
10 16 2 i I
0 27 0 args [Ljava/lang/String;
8 19 1 nativeLoad Lcom/kaimingwan/questions/NativeLoad;
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 10
locals = [ class com/kaimingwan/questions/NativeLoad, int ]
frame_type = 250 /* chop */
offset_delta = 15

public void useNativeClassLoad();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: ldc #5 // class com/kaimingwan/questions/model/Student
2: invokevirtual #6 // Method java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;
5: astore_1
6: return
LineNumberTable:
line 19: 0
line 20: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/kaimingwan/questions/NativeLoad;
6 1 1 clzLoader Ljava/lang/ClassLoader;
}
SourceFile: "NativeLoad.java"

StaticLoad

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
package com.kaimingwan.questions;

import com.kaimingwan.questions.model.Student;

/**
* @author wanshao create time is 2022/2/23
**/
public class StaticLoad {

private static ClassLoader staticStuClzLoader = Student.class.getClassLoader();

public static void main(String[] args) {
StaticLoad staticLoad = new StaticLoad();
for (int i = 0; i < 100; i++) {
staticLoad.useStaticClassLoad();
}

}


public void useStaticClassLoad() {
ClassLoader clzLoader = staticStuClzLoader;
}
}

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
Classfile /Users/wanshao/projects/java-questions/target/classes/com/kaimingwan/questions/StaticLoad.class
Last modified 2022-2-25; size 909 bytes
MD5 checksum f7da301c6a526fb272a2339ece5ffcd4
Compiled from "StaticLoad.java"
public class com.kaimingwan.questions.StaticLoad
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#32 // java/lang/Object."<init>":()V
#2 = Class #33 // com/kaimingwan/questions/StaticLoad
#3 = Methodref #2.#32 // com/kaimingwan/questions/StaticLoad."<init>":()V
#4 = Methodref #2.#34 // com/kaimingwan/questions/StaticLoad.useStaticClassLoad:()V
#5 = Fieldref #2.#35 // com/kaimingwan/questions/StaticLoad.staticStuClzLoader:Ljava/lang/ClassLoader;
#6 = Class #36 // com/kaimingwan/questions/model/Student
#7 = Methodref #37.#38 // java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;
#8 = Class #39 // java/lang/Object
#9 = Utf8 staticStuClzLoader
#10 = Utf8 Ljava/lang/ClassLoader;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/kaimingwan/questions/StaticLoad;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 i
#21 = Utf8 I
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 staticLoad
#25 = Utf8 StackMapTable
#26 = Class #33 // com/kaimingwan/questions/StaticLoad
#27 = Utf8 useStaticClassLoad
#28 = Utf8 clzLoader
#29 = Utf8 <clinit>
#30 = Utf8 SourceFile
#31 = Utf8 StaticLoad.java
#32 = NameAndType #11:#12 // "<init>":()V
#33 = Utf8 com/kaimingwan/questions/StaticLoad
#34 = NameAndType #27:#12 // useStaticClassLoad:()V
#35 = NameAndType #9:#10 // staticStuClzLoader:Ljava/lang/ClassLoader;
#36 = Utf8 com/kaimingwan/questions/model/Student
#37 = Class #40 // java/lang/Class
#38 = NameAndType #41:#42 // getClassLoader:()Ljava/lang/ClassLoader;
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/Class
#41 = Utf8 getClassLoader
#42 = Utf8 ()Ljava/lang/ClassLoader;
{
public com.kaimingwan.questions.StaticLoad();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/kaimingwan/questions/StaticLoad;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/kaimingwan/questions/StaticLoad
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: bipush 100
13: if_icmpge 26
16: aload_1
17: invokevirtual #4 // Method useStaticClassLoad:()V
20: iinc 2, 1
23: goto 10
26: return
LineNumberTable:
line 13: 0
line 14: 8
line 15: 16
line 14: 20
line 18: 26
LocalVariableTable:
Start Length Slot Name Signature
10 16 2 i I
0 27 0 args [Ljava/lang/String;
8 19 1 staticLoad Lcom/kaimingwan/questions/StaticLoad;
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 10
locals = [ class com/kaimingwan/questions/StaticLoad, int ]
frame_type = 250 /* chop */
offset_delta = 15

public void useStaticClassLoad();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: getstatic #5 // Field staticStuClzLoader:Ljava/lang/ClassLoader;
3: astore_1
4: return
LineNumberTable:
line 22: 0
line 23: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/kaimingwan/questions/StaticLoad;
4 1 1 clzLoader Ljava/lang/ClassLoader;

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #6 // class com/kaimingwan/questions/model/Student
2: invokevirtual #7 // Method java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;
5: putstatic #5 // Field staticStuClzLoader:Ljava/lang/ClassLoader;
8: return
LineNumberTable:
line 10: 0
}
SourceFile: "StaticLoad.java"

从汇编结果来看,两者差异主要是指令层面使用的差异。staic load 使用了 GETSTATIC,而 native load 则使用了 invoke special。但是他们本质上都是同一个方法引用的调用,所以两者本质上使用不会有太大的差别。

1
java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;