前言
今天工作中与同事就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;@Slf4j public class StaticFieldPerfTest { private static ClassLoader staticStuClzLoader = Student.class.getClassLoader(); @Test public void useStaticClassLoad () { int count = 1000000 ; Stopwatch stopwatch = Stopwatch.createStarted(); 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(); 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;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 #2 = Class #30 #3 = Methodref #2. #29 #4 = Methodref #2. #31 #5 = Class #32 #6 = Methodref #33. #34 #7 = Class #35 #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 #24 = Utf8 useNativeClassLoad #25 = Utf8 clzLoader #26 = Utf8 Ljava/lang/ClassLoader; #27 = Utf8 SourceFile #28 = Utf8 NativeLoad.java #29 = NameAndType #8 :#9 #30 = Utf8 com/kaimingwan/questions/NativeLoad #31 = NameAndType #24 :#9 #32 = Utf8 com/kaimingwan/questions/model/Student #33 = Class #36 #34 = NameAndType #37 :#38 #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 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 3 : dup 4 : invokespecial #3 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 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 offset_delta = 10 locals = [ class com /kaimingwan/questions/NativeLoad, int ] frame_type = 250 offset_delta = 15 public void useNativeClassLoad () ; descriptor: ()V flags: ACC_PUBLIC Code: stack=1 , locals=2 , args_size=1 0 : ldc #5 2 : invokevirtual #6 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;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 #2 = Class #33 #3 = Methodref #2. #32 #4 = Methodref #2. #34 #5 = Fieldref #2. #35 #6 = Class #36 #7 = Methodref #37. #38 #8 = Class #39 #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 #27 = Utf8 useStaticClassLoad #28 = Utf8 clzLoader #29 = Utf8 <clinit> #30 = Utf8 SourceFile #31 = Utf8 StaticLoad.java #32 = NameAndType #11 :#12 #33 = Utf8 com/kaimingwan/questions/StaticLoad #34 = NameAndType #27 :#12 #35 = NameAndType #9 :#10 #36 = Utf8 com/kaimingwan/questions/model/Student #37 = Class #40 #38 = NameAndType #41 :#42 #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 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 3 : dup 4 : invokespecial #3 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 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 offset_delta = 10 locals = [ class com /kaimingwan/questions/StaticLoad, int ] frame_type = 250 offset_delta = 15 public void useStaticClassLoad () ; descriptor: ()V flags: ACC_PUBLIC Code: stack=1 , locals=2 , args_size=1 0 : getstatic #5 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 2 : invokevirtual #7 5 : putstatic #5 8 : return LineNumberTable: line 10 : 0 } SourceFile: "StaticLoad.java"
从汇编结果来看,两者差异主要是指令层面使用的差异。staic load使用了GETSTATIC,而native load则使用了invoke special。但是他们本质上都是同一个方法引用的调用,所以两者本质上使用不会有太大的差别。
1 java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;