Java类的加载--对性能的影响!

492 阅读3分钟

Java类的加载--对性能的影响!

发布者::Ram Lakshmanan inCore Java May 14th, 2022 0 Views

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的目的是什么?

通常情况下,如果我们想实例化一个新的对象,我们会像这样写代码。

 new io.ycrash.DummyObject();

然而,你可以使用ClassLoader.loadClass() API,也可以实例化对象。下面是代码的样子。

 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
 Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
 myClass.newInstance();

你可以注意到在第2行'classLoader.loadClass()'被调用。这一行将把'io.ycrash.DummyObject'类加载到内存中。在第3行中,'io.ycrash.DummyObject'类被使用'newInstance()'API进行实例化。

这种实例化对象的方式就像用手摸鼻子一样,通过脖子后面。你可能想知道为什么有人会这样做?只有当你在写代码的时候知道类的名字,你才能用'new'来实例化对象。在某些情况下,你可能只在运行时知道类的名称。例如,如果你正在编写框架(像Spring框架,XML解析器,...),你将只在运行时知道要实例化的类名。在编写代码时,你将不知道你将实例化哪些类。在这种情况下,你将不得不最终使用'ClassLoader.loadClass()'API。

在哪里使用'ClassLoader.loadClass()'?

ClassLoader.loadClass() "被用于几个流行的第三方库、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)
:
:

Google 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驱动,将使用'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类的加载有了足够的了解。现在是时候研究其性能影响了。为了方便我们的研究,我创建了这个简单的程序。

package io.ycrash.classloader;
 
 public class MyApp extends Thread {
    
   @Override
   public void run() {
       
       try {
          
          while (true) {
             
             ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
             Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
             myClass.newInstance();
          }      
       } catch (Exception e) {
          
       }
   }
    
   public static void main(String args[]) throws Exception {
       
       for (int counter = 0; counter < 10; ++counter) {
          
          new MyApp().start();
       }
   }
 }

如果你注意到这个程序,我正在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封锁。下面是一个封锁状态的线程(即线程-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

你可以注意到,'Thread-9'在java.lang.ClassLoader.loadClass()方法上被封锁了。它正在等待获取'<0x00000003db200ae0>'上的锁。所有其他处于BLOCKED状态的8个线程也有完全相同的堆栈轨迹。

下面是'Thread-6'的堆栈跟踪,它封锁了所有其他9个线程。

 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()时,线程会变成BLOCKED?

要理解为什么线程在调用'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个线程将被置入阻塞状态。

下面是'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状态。如果我们能在应用程序启动时只加载一次该类,那么这个问题就可以得到解决。这可以通过修改代码来实现,如下图所示。

  package io.ycrash.classloader;
 
 public class MyApp extends Thread {
   
   private Class<?≶ myClass = initClass();
   
   private Class<?≶ initClass() {
      
      try {         
         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
         return classLoader.loadClass("io.ycrash.DummyObject");
      } catch (Exception e) {         
      }      
      
      return null;
   }
   
   @Override
   public void run() {
      
      while (true) {
      
         try {            
            myClass.newInstance();
         } catch (Exception e) {         
         }
     }
   }
   
   public static void main(String args[]) throws Exception {
      
      for (int counter = 0; counter < 10; ++counter) {
        
         new MyApp().start();
      }
   }
 }


对代码的修改解决了这个问题。如果你看到现在'myClass'在第5行被初始化了。与早期的方法不同,myClass在每次循环迭代中都被初始化,现在myClass只在线程实例化时被初始化一次。由于代码中的这一转变,"ClassLoader.loadClass() "API将不会被多次调用。因此,它将防止线程进入BLOCKED状态。

解决方案

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

a.尝试看看你是否可以在应用程序启动时而不是运行时调用'ClassLoader.loadClass()'API。

b.如果你的应用程序在运行时反复加载同一个类,那么试着只加载一次该类。在那之后,缓存该类并重新使用它,如上例所示。

c.使用故障排除工具,如fastThread,yCrash, ...来检测哪个框架或第三方库或代码路径触发了这个问题。检查框架是否在其最新版本中给出了任何修复,如果是的话,就升级到最新版本。

由我们JCG项目的合伙人Ram Lakshmanan授权发表在Java Code Geeks上。点击这里查看原文。Java类的加载--性能影响!

Java Code Geeks撰稿人所表达的观点仅代表其本人。

2022-05-14

拉姆-拉克什马南