在这篇文章中,让我们更多地了解这个 API 及其对性能的影响。
扫码关注《Java学研大本营》,加入读者群,分享更多精彩
java.lang.ClassLoader#loadClass() API被第三方库、JDBC 驱动程序、框架、应用服务器用于将 java 类加载到内存中。应用程序开发人员不经常使用此 API。然而,当他们使用诸如“java.lang.Class.forName()”或“org.springframework.util.ClassUtils.forName()”之类的API时,他们在内部调用这个“java.lang.ClassLoader#loadClass()”API .
在运行时在不同线程中频繁使用此 API 会降低应用程序的性能。有时它甚至可以使整个应用程序无响应。在这篇文章中,让我们更多地了解这个 API 及其对性能的影响。
'ClassLoader.loadClass()' API 的目的是什么?
通常,如果我们想实例化一个新对象,我们会这样编写代码:
1. new io.ycrash.DummyObject();
但是,您可以使用 ClassLoader.loadClass() API 并实例化对象。以下是代码的外观:
1. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
2. Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
3. myClass.newInstance();
您可以注意到在第 2 行中调用了“classLoader.loadClass()”。此行会将“io.ycrash.DummyObject”类加载到内存中。在第 3 行中,“io.ycrash.DummyObject”类使用“newInstance()”API 进行实例化。
这种实例化对象的方式就像用手触摸鼻子,穿过脖子后面。你可能想知道为什么有人会这样做?仅当您在编写代码时知道类的名称时,才能使用“new”实例化对象。在某些情况下,您可能仅在运行时才知道类的名称。例如,如果您正在编写框架(如 Spring Framework、XML 解析器等),您将知道仅在运行时要实例化的类名。在编写代码时,您将不知道要实例化哪些类。在这种情况下,您最终将不得不使用“ClassLoader.loadClass()”API。
在哪里使用“ClassLoader.loadClass()”?
'ClassLoader.loadClass()' 用于几个流行的 3rd 方库、JDBC 驱动程序、框架和应用程序服务器。本节重点介绍一些使用“ClassLoader.loadClass()”API 的流行框架。
Apache Xalan
当您使用Apache Xalan框架序列化和反序列化 XML 时,将使用“ClassLoader.loadClass()”API。下面是使用 Apache Xalan 框架中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
at java.lang.ClassLoader.loadClass(ClassLoader.java:404)
- locked <0x6d497769> (a com.wm.app.b2b.server.ServerClassLoader)
at com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1175)
at com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1108)
at org.apache.xml.serializer.ObjectFactory.findProviderClass(ObjectFactory.java:503)
at org.apache.xml.serializer.SerializerFactory.getSerializer(SerializerFactory.java:129)
at org.apache.xalan.transformer.TransformerIdentityImpl.createResultContentHandler(TransformerIdentityImpl.java:260)
at org.apache.xalan.transformer.TransformerIdentityImpl.transform(TransformerIdentityImpl.java:330)
at org.springframework.ws.client.core.WebServiceTemplate$4.extractData(WebServiceTemplate.java:441)
:
:
谷歌 GUICE 框架
当您使用Google GUICE 框架时,将使用 'ClassLoader.loadClass()' API。下面是使用 Google GUICE 框架中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
at java.lang.Object.wait(Native Method)
- waiting on hudson.remoting.RemoteInvocationHandler$RPCRequest@1e408f0
at hudson.remoting.Request.call(Request.java:127)
at hudson.remoting.RemoteInvocationHandler.invoke(RemoteInvocationHandler.java:160)
at $Proxy5.fetch2(Unknown Source)
at hudson.remoting.RemoteClassLoader.findClass(RemoteClassLoader.java:122)
at java.lang.ClassLoader.loadClass(ClassLoader.java:321)
- locked hudson.remoting.RemoteClassLoader@15c7850
at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
at com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:69)
at com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:43)
at com.google.inject.internal.BindingImpl.acceptVisitor(BindingImpl.java:93)
at com.google.inject.internal.AbstractProcessor.process(AbstractProcessor.java:56)
at com.google.inject.internal.InjectorShell$Builder.build(InjectorShell.java:183)
at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:104)
- locked com.google.inject.internal.InheritingState@1c915a5
at com.google.inject.Guice.createInjector(Guice.java:94)
at com.google.inject.Guice.createInjector(Guice.java:71)
at com.google.inject.Guice.createInjector(Guice.java:61)
:
:
Oracle JDBC 驱动程序
如果您使用Oracle JDBC Driver,将使用“ClassLoader.loadClass()”API。下面是使用 Oracle JDBC 驱动程序中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
at com.ibm.ws.classloader.CompoundClassLoader.loadClass(CompoundClassLoader.java:482)
- waiting to lock <0xffffffff11a5f7d8> (a com.ibm.ws.classloader.CompoundClassLoader)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:170)
at oracle.jdbc.driver.PhysicalConnection.safelyGetClassForName(PhysicalConnection.java:4682)
at oracle.jdbc.driver.PhysicalConnection.addClassMapEntry(PhysicalConnection.java:2750)
at oracle.jdbc.driver.PhysicalConnection.addDefaultClassMapEntriesTo(PhysicalConnection.java:2739)
at oracle.jdbc.driver.PhysicalConnection.initializeClassMap(PhysicalConnection.java:2443)
at oracle.jdbc.driver.PhysicalConnection.ensureClassMapExists(PhysicalConnection.java:2436)
:
:
AspectJ 库
如果您使用AspectJ 库,将使用 'ClassLoader.loadClass()' API。下面是使用 AspectJ 框架中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
:
:
at java.base@11.0.7/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
at java.base@11.0.7/java.lang.Class.forName0(Native Method)
at java.base@11.0.7/java.lang.Class.forName(Class.java:398)
at app//org.aspectj.weaver.reflect.ReflectionBasedReferenceTypeDelegateFactory.createDelegate(ReflectionBasedReferenceTypeDelegateFactory.java:38)
at app//org.aspectj.weaver.reflect.ReflectionWorld.resolveDelegate(ReflectionWorld.java:195)
at app//org.aspectj.weaver.World.resolveToReferenceType(World.java:486)
at app//org.aspectj.weaver.World.resolve(World.java:321)
- locked java.lang.Object@1545fe7d
at app//org.aspectj.weaver.World.resolve(World.java:231)
at app//org.aspectj.weaver.World.resolve(World.java:436)
at app//org.aspectj.weaver.internal.tools.PointcutExpressionImpl.couldMatchJoinPointsInType(PointcutExpressionImpl.java:83)
at org.springframework.aop.aspectj.AspectJExpressionPointcut.matches(AspectJExpressionPointcut.java:275)
at org.springframework.aop.support.AopUtils.canApply(AopUtils.java:225)
:
:
研究性能影响
现在我假设您已经对 Java 类加载有足够的了解。现在是时候研究它对性能的影响了。为了方便我们的学习,我创建了这个简单的程序:
1. package io.ycrash.classloader;
2.
3. public class MyApp extends Thread {
4.
5. @Override
6. public void run() {
7.
8. try {
9.
10. while (true) {
11.
12. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
13. Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
14. myClass.newInstance();
15. }
16. } catch (Exception e) {
17.
18. }
19. }
20.
21. public static void main(String args[]) throws Exception {
22.
23. for (int counter = 0; counter < 10; ++counter) {
24.
25. new MyApp().start();
26. }
27. }
28. }
2.
3. public class MyApp extends Thread {
4.
5. @Override
6. public void run() {
7.
8. try {
9.
10. while (true) {
11.
12. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
13. Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
14. myClass.newInstance();
15. }
16. } catch (Exception e) {
17.
18. }
19. }
20.
21. public static void main(String args[]) throws Exception {
22.
23. for (int counter = 0; counter < 10; ++counter) {
24.
25. new MyApp().start();
26. }
27. }
28. }
如果你注意.ycrash.classloader; 2. 3. public class MyApp extends Thread { 4.
5. @Override 6. public void run() { 7.
8. try { 9.
10. while (true) { 11.
12. ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 13. Class myClass = classLoader.loadClass("io.ycrash.DummyObject"); 14. myClass.newInstance(); 15. } 16. } catch (Exception e) { 17. 18. } 19. } 20. 21. public static void main(String args[]) throws Exception { 22. 23. for (int counter = 0; counter < 10; ++counter) { 24. 25. new MyApp().start(); 26. } 27. } 28. }1. package io.ycrash.classloader; 2. 3. public class MyApp extends Thread { 4. 5. @Override 6. public void run() { 7. 8. try { 9. 10. while (true) { 11. 12. ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 13. Class myClass = classLoader.loadClass("io.ycrash.DummyObject"); 14. myClass.newInstance(); 15. }
16. } catch (Exception e) { 17.
18. } 19. } 20.
21. public static void main(String args[]) throws Exception { 22.
23. for (int counter = 0; counter < 10; ++counter) { 24.
25. new MyApp().start(); 26. } 27. } 28. }
如果你注意到这个程序,我在 main() 方法中创建了 10 个线程。
每个线程都进入一个无限循环并在 run() 方法中实例化 'io.ycrash.DummyObject',使用第 13 行中的 'classLoader.loadClass()' API。这意味着 'classLoader.loadClass()' 将所有这 10 个线程一次又一次地调用。
ClassLoader.loadClass() — 阻塞线程
我们执行了上面的程序。在程序执行时,我们运行了开源yCrash 脚本。该脚本从应用程序中捕获 360 度数据(线程转储、GC 日志、堆转储、netstat、VMstat、iostat、top、内核日志……)。我们使用fastThread(一种线程转储分析工具)分析了捕获的线程转储。此工具为该程序生成的线程转储分析报告可在此处找到。工具报告 10 个线程中有 9 个处于 BLOCKED 状态。如果线程处于 BLOCKED 状态,则表明它被卡在了资源上。当它处于 BLOCKED 状态时,它不会前进。它会妨碍应用程序的性能。您可能想知道——为什么上面的简单程序会使线程进入 BLOCKED 状态。
图:显示 9 个 BLOCKED 线程的传递图(由fastThread生成)
以上是线程转储分析报告的摘录。您可以看到 9 个线程('Thread-0'、'Thread-1'、'Thread-2'、'Thread-3'、'Thread-4'、'Thread-5'、'Thread-7'、' Thread-8', 'Thread-9') 被 'Thread-6' 阻塞。下面是一个 BLOCKED 状态线程(即 Thread-9)的堆栈跟踪:
Thread-9
Stack Trace is:
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ClassLoader.loadClass(ClassLoader.java:404)
- waiting to lock <0x00000003db200ae0> (a java.lang.Object)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at io.ycrash.classloader.MyApp.run(MyApp.java:13)
Locked ownable synchronizers:
- None
您会注意到 java.lang.ClassLoader.loadClass() 方法上的“Thread-9”被 BLOCKED。它正在等待获取“<0x00000003db200ae0>”上的锁。所有其他处于 BLOCKED 状态的其余 8 个线程也具有完全相同的堆栈跟踪。
下面是阻塞所有其他 9 个线程的 'Thread-6' 的堆栈跟踪:
Thread-6
java.lang.Thread.State: RUNNABLE
at java.lang.ClassLoader.findLoadedClass0(Native Method)
at java.lang.ClassLoader.findLoadedClass(ClassLoader.java:1038)
at java.lang.ClassLoader.loadClass(ClassLoader.java:406)
- locked <0x00000003db200ae0> (a java.lang.Object)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at io.ycrash.classloader.MyApp.run(MyApp.java:13)
Locked ownable synchronizers:
- None
您可以注意到'Thread-6' 能够获得锁(即'<0x00000003db200ae0>')并继续前进。但是,所有其他 9 个线程都在等待获取此锁。
为什么调用 ClassLoader.loadClass() 时线程会阻塞?
要了解为什么线程在调用 'ClassLoader.loadClass()' 方法时会进入 BLOCKED 状态,我们将不得不查看它的源代码。下面是 ClassLoader.loadClass() 方法的源代码摘录。如果您想查看 java.lang.ClassLoader 的完整源代码,可以参考这里:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
:
:
在源代码突出显示的行中,您将看到“同步”代码块的用法。当一个代码块被同步时,只允许一个线程进入该块。在我们上面的示例中,有 10 个线程正在尝试同时访问“ClassLoader.loadClass()”。只允许一个线程进入同步代码块,剩下的 9 个线程将进入 BLOCKED 状态。
下面是“getClassLoadingLock()”方法的源代码,它返回一个对象并在该对象上发生同步。
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
您会注意到,“getClassLoadingLock()”方法每次都会为相同的类名返回相同的对象。即,如果类名是“io.ycrash.DummyObject”——它每次都会返回相同的对象。因此,所有 10 个线程都将返回同一个对象。在这一个对象上,将发生同步。它会将所有线程置于 BLOCKED 状态。
如何解决这个问题?
之所以会出现此问题,是因为在每次循环迭代时都会再次加载“io.ycrash.DummyObject”类。这会导致线程进入 BLOCKED 状态。如果我们在应用程序启动期间只能加载一次类,则可以解决此问题。这可以通过如下所示修改代码来实现。
1. package io.ycrash.classloader;
2.
3. public class MyApp extends Thread {
4.
5. private Class<?> myClass = initClass();
6.
7. private Class<?> initClass() {
8.
9. try {
10. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
11. return classLoader.loadClass("io.ycrash.DummyObject");
12. } catch (Exception e) {
13. }
14.
15. return null;
16. }
17.
18. @Override
19. public void run() {
20.
21. while (true) {
22.
23. try {
24. myClass.newInstance();
25. } catch (Exception e) {
26. }
27. }
28. }
29.
30. public static void main(String args[]) throws Exception {
31.
32. for (int counter = 0; counter < 10; ++counter) {
33.
34. new MyApp().start();
35. }
36. }
37. }
进行此代码更改解决了该问题。如果您现在看到 'myClass' 在第 5 行被初始化。与之前在每次循环迭代中初始化 myClass 的方法不同,现在 myClass 在实例化线程时仅初始化一次。由于代码中的这种转变,'ClassLoader.loadClass()' API 将不会被多次调用。因此它将阻止线程进入 BLOCKED 状态。
解决方案
如果您的应用程序也遇到这个类加载性能问题,那么这里是解决它的潜在解决方案。
-
尝试查看是否可以在应用程序启动时而不是运行时调用“ClassLoader.loadClass()”API。
-
如果您的应用程序在运行时一次又一次地加载同一个类,则尝试仅加载一次该类。在那之后,缓存该类并重新使用它,如上例所示。
-
使用诸如fastThread、yCrash等故障排除工具来检测是哪个框架或第三方库或代码路径触发了问题。检查框架是否在其最新版本中提供了任何修复,如果有,请升级到最新版本。
参考文章: https://medium.com/@RamLakshmanan/java-class-loading-performance-impact-44bf84fe4c2a
推荐书单
1.《项目驱动零起点学Java》
购买链接:https://item.jd.com/13607758.html
《项目驱动零起点学Java》贯穿6个完整项目,经过作者多年教学经验提炼而得,项目从小到大、从短到长,可以让读者在练习项目的过程中,快速掌握一系列知识点。
作者是国内知名Java教学者和传播者,一路披荆斩棘,兢兢业业20余年。积累了丰富的“培”“训”经验,也产出了很多优质的教学理论。
Java语言经过数十年的发展,体系逐渐变得庞大而复杂,本书芟繁就简,提炼出了最为重要的知识点,可以让读者轻松上手。本书配套有专栏课程,课程中提供了扩展内容。
《项目驱动零起点学Java》共分 13 章,围绕 6 个项目和 258 个代码示例,分别介绍了走进Java 的世界、变量与数据类型、运算符、流程控制、方法、数组、面向对象、异常、常用类、集合、I/O流、多线程、网络编程相关内容。《项目驱动零起点学Java》总结了马士兵老师从事Java培训十余年来经受了市场检验的教研成果,通过6 个项目以及每章的示例和习题,可以帮助读者快速掌握Java 编程的语法以及算法实现。扫描每章提供的二维码可观看相应章节内容的视频讲解。
2.《Java编程讲义》
购买链接:https://item.jd.com/13495830.html
《Java编程讲义》根据目前Java开发领域的实际需求,从初学者角度出发,详细讲解了Java技术的基础知识。
全书共15章,包括Java开发入门,Java语言基础,Java控制结构,数组,面向对象编程,继承和多态,抽象类、接口和内部类,异常处理,Java常用类库,集合与泛型,Lambda表达式,输入-输出流,多线程,JDBC数据库技术,网络编程等内容。内容全面覆盖.1ava开发必备的基础知识点,结合生活化案例展开讲解,程序代码给出了详细的注释,能够使初学者轻松领会Java技术精髓,快速掌握Java开发技能。
《Java编程讲义》适合作为高等院校相关专业的教材及教学参考书,也适合作为Java开发入门者的自学用书,还可供开发人员查阅、参考。
精彩回顾
扫码关注《Java学研大本营》,加入读者群,分享更多精彩