性能文章>【译】Java的类加载会带来哪些性能问题?如何解决?>

【译】Java的类加载会带来哪些性能问题?如何解决?转载

1年前
305415

java.lang.ClassLoader #loadClass() API被 3rd 方库、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框架序列化和反序列化 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. }

如果你注意到这个程序,我在 main() 方法中创建了 10 个线程。 

每个线程都进入一个无限循环并在 run() 方法中实例化 'io.ycrash.DummyObject',使用第 13 行中的 'classLoader.loadClass()' API。这意味着 'classLoader.loadClass()' 将所有这 10 个线程一次又一次地调用。

ClassLoader.loadClass() – 阻塞线程

 我们执行了上面的程序。在程序执行时,我们运行了开源yCrash 脚本。此脚本从应用程序中捕获 360 度数据(线程转储、GC 日志、堆转储、netstat、VMstat、iostat、top、内核日志……)。我们使用线程转储分析工具fastThread分析dump文件,此工具为该程序生成的线程转储分析报告可在此处找到。工具报告 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 状态。

解决方案

如果您的应用程序也遇到这个类加载性能问题,那么这里是解决它的潜在解决方案。

1,尝试查看是否可以在应用程序启动时而不是运行时调用“ClassLoader.loadClass()”API。

2,如果程序在运行时一次又一次地加载同一个类,则尝试仅加载一次该类。在那之后,缓存该类并重新使用它,如上例所示。

3,使用诸如fastThreadyCrash等故障排除工具来检测是哪个框架或 3rd 方库或代码路径触发了问题。检查框架是否在其最新版本中提供了任何修复,如果有,请升级到最新版本。

原文地址:https://blog.fastthread.io/2022/05/03/java-class-loading-performance-impact/

原文作者:fastthread

点赞收藏
willberthos

keep foolish!

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

为什么要做代码Review?

为什么要做代码Review?

日常Bug排查-集群逐步失去响应

日常Bug排查-集群逐步失去响应

浅析AbstractQueuedSynchronizer

浅析AbstractQueuedSynchronizer

5
1