Java 类加载:性能影响

java.lang.ClassLoader#loadClass() API 由第三方库、JDBC 驱动程序、框架和应用程序服务器使用,将 java 类加载到内存中。应用程序开发人员不经常使用此 API。但是,当他们使用诸如“”或“”之类的API时,他们会在内部调用此“”API。java.lang.Class.forName()org.springframework.util.ClassUtils.forName()java.lang.ClassLoader#loadClass()

在运行时,在不同线程之间频繁使用此 API 可能会降低应用程序性能。有时它甚至会使整个应用程序无响应。在这篇文章中,让我们进一步了解这个API及其性能影响。

'classloader.loadclass()' API 的用途是什么?

通常,如果我们想实例化一个新对象,我们会像这样编写代码:

new io.ycrash.DummyObject();

但是,您可以使用 ClassLoader.loadClass() API 并实例化对象。以下是代码的外观:

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

您可以注意到在第 2 行中调用了 “”。此行将 '' 类加载到内存中。在第 3 行中,“”类使用“”API 实例化。classLoader.loadClass()io.ycrash.DummyObjectio.ycrash.DummyObjectnewInstance()

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

哪里使用'classloader.loadclass()'?

''用于几个流行的第三方库,JDBC驱动程序,框架和应用程序服务器。本节重点介绍一些使用“”API的流行框架。ClassLoader.loadClass()ClassLoader.loadClass()

Apache Xalan

当您使用Apache Xalan框架序列化和反序列化XML时,将使用“”API。下面是使用 Apache Xalan 框架中的 '' API 的线程的堆栈跟踪。ClassLoader.loadClass()ClassLoader.loadClass()

at java.lang.ClassLoader.loadClass(ClassLoader.java:404) br- locked <0x6d497769> (a com.wm.app.b2b.server.ServerClassLoader) brat com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1175) brat com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1108) brat org.apache.xml.serializer.ObjectFactory.findProviderClass(ObjectFactory.java:503) brat org.apache.xml.serializer.SerializerFactory.getSerializer(SerializerFactory.java:129) brat org.apache.xalan.transformer.TransformerIdentityImpl.createResultContentHandler(TransformerIdentityImpl.jbrava:260) brat org.apache.xalan.transformer.TransformerIdentityImpl.transform(TransformerIdentityImpl.java:330) brat org.springframework.ws.client.core.WebServiceTemplate$4.extractData(WebServiceTemplate.java:441) br:br:

谷歌 GUICE 框架

当您使用Google GUICE框架时,将使用“”API。下面是使用Google GUICE框架中的“”API的线程的堆栈跟踪。ClassLoader.loadClass()ClassLoader.loadClass()

at java.lang.Object.wait(Native Method) br-  waiting on hudson.remoting.RemoteInvocationHandler$RPCRequest@1e408f0 brat hudson.remoting.Request.call(Request.java:127) brat hudson.remoting.RemoteInvocationHandler.invoke(RemoteInvocationHandler.java:160) brat $Proxy5.fetch2(Unknown Source) at hudson.remoting.RemoteClassLoader.findClass(RemoteClassLoader.java:122) brat java.lang.ClassLoader.loadClass(ClassLoader.java:321) br-  locked hudson.remoting.RemoteClassLoader@15c7850 brat java.lang.ClassLoader.loadClass(ClassLoader.java:266) brat com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:69) brat com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:43) brat com.google.inject.internal.BindingImpl.acceptVisitor(BindingImpl.java:93) brat com.google.inject.internal.AbstractProcessor.process(AbstractProcessor.java:56) brat com.google.inject.internal.InjectorShell$Builder.build(InjectorShell.java:183) brat com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:104) br-  locked com.google.inject.internal.InheritingState@1c915a5 brat com.google.inject.Guice.createInjector(Guice.java:94) brat com.google.inject.Guice.createInjector(Guice.java:71) brat com.google.inject.Guice.createInjector(Guice.java:61) br:br:

Oracle JDBC Driver

如果您使用 Oracle JDBC 驱动程序,则将使用“”API。下面是使用 Oracle JDBC 驱动程序中的 '' API 的线程的堆栈跟踪。ClassLoader.loadClass()ClassLoader.loadClass()

at com.ibm.ws.classloader.CompoundClassLoader.loadClass(CompoundClassLoader.java:482) br- waiting to lock <0xffffffff11a5f7d8> (a com.ibm.ws.classloader.CompoundClassLoader) brat java.lang.ClassLoader.loadClass(ClassLoader.java:247) at java.lang.Class.forName0(Native Method) brat java.lang.Class.forName(Class.java:170) brat oracle.jdbc.driver.PhysicalConnection.safelyGetClassForName(PhysicalConnection.java:4682) brat oracle.jdbc.driver.PhysicalConnection.addClassMapEntry(PhysicalConnection.java:2750) brat oracle.jdbc.driver.PhysicalConnection.addDefaultClassMapEntriesTo(PhysicalConnection.java:2739) brat oracle.jdbc.driver.PhysicalConnection.initializeClassMap(PhysicalConnection.java:2443) brat oracle.jdbc.driver.PhysicalConnection.ensureClassMapExists(PhysicalConnection.java:2436) br:br:

AspectJ 库

如果您使用AspectJ库,将使用“”API。下面是使用 AspectJ 框架中的 '' API 的线程的堆栈跟踪。ClassLoader.loadClass()ClassLoader.loadClass()

: br:brat java.base@11.0.7/java.lang.ClassLoader.loadClass(ClassLoader.java:522) brat java.base@11.0.7/java.lang.Class.forName0(Native Method) brat java.base@11.0.7/java.lang.Class.forName(Class.java:398) brat brapp//org.aspectj.weaver.reflect.ReflectionBasedReferenceTypeDelegateFactory.createDelegate(ReflectionBasebrdReferenceTypeDelegateFactory.java:38) brat app//org.aspectj.weaver.reflect.ReflectionWorld.resolveDelegate(ReflectionWorld.java:195) brat app//org.aspectj.weaver.World.resolveToReferenceType(World.java:486) brat app//org.aspectj.weaver.World.resolve(World.java:321) br- locked java.lang.Object@1545fe7d brat app//org.aspectj.weaver.World.resolve(World.java:231) brat app//org.aspectj.weaver.World.resolve(World.java:436) brat brapp//org.aspectj.weaver.internal.tools.PointcutExpressionImpl.couldMatchJoinPointsInType(PointcutExpressibronImpl.java:83) brat org.springframework.aop.aspectj.AspectJExpressionPointcut.matches(AspectJExpressionPointcut.java:275) brat org.springframework.aop.support.AopUtils.canApply(AopUtils.java:225) br:br:

研究性能影响

现在我假设你已经对Java类加载有了足够的了解。现在是时候研究其性能影响了。为了方便我们的学习,我创建了这个简单的程序:

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

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

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

Classloader.loadclass() – Blocked Threads

我们执行了上述程序。当程序执行时,我们运行开源yCrash脚本。此脚本从应用程序捕获 360 度数据(线程转储、GC 日志、堆转储、netstat、VMstat、iostat、top、内核日志,...)。我们使用 fastThread(一种线程转储分析工具)分析捕获的线程转储。可以在此处找到此工具为此程序生成的线程转储分析报告。工具报告 10 个线程中有 9 个处于“已阻止”状态。如果线程处于“已阻止”状态,则表示该线程因资源而卡住。当它处于“已阻止”状态时,它不会向前推进。这将影响应用程序的性能。您可能想知道 - 为什么上述简单程序使线程进入阻塞状态?



图:显示 9 个被阻塞线程的传递图(由 fastThread 生成)

以上是线程转储分析报告的摘录。您可以看到 9 个线程(“Thread-0”、“Thread-1”、“Thread-2”、“Thread-3”、“Thread-4”、“Thread-5”、“Thread-7”、“Thread-8”、“Thread-9”)被“Thread-6”阻止。下面是一个被阻塞状态线程(即 Thread-9)的堆栈跟踪:

Thread-9 brStack Trace is: brjava.lang.Thread.State: BLOCKED (on object monitor) brat java.lang.ClassLoader.loadClass(ClassLoader.java:404) br- waiting to lock <0x00000003db200ae0> (a java.lang.Object) brat sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) brat java.lang.ClassLoader.loadClass(ClassLoader.java:357) brat io.ycrash.classloader.MyApp.run(MyApp.java:13) brLocked ownable synchronizers: br- None

您可以注意到“Thread-9”在方法上被阻止。它正在等待获得“<0x00000003db200ae0>”的锁定。处于“已阻止”状态的所有其他 8 个线程也具有完全相同的堆栈跟踪。java.lang.ClassLoader.loadClass()

以下是阻塞所有其他 9 个线程的“Thread-6”的堆栈跟踪:

Thread-6 brjava.lang.Thread.State: RUNNABLE brat java.lang.ClassLoader.findLoadedClass0(Native Method) brat java.lang.ClassLoader.findLoadedClass(ClassLoader.java:1038) brat java.lang.ClassLoader.loadClass(ClassLoader.java:406) br- locked <0x00000003db200ae0> (a java.lang.Object) brat sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) brat java.lang.ClassLoader.loadClass(ClassLoader.java:357) brat io.ycrash.classloader.MyApp.run(MyApp.java:13) brLocked ownable synchronizers: br- None

您可以注意到“Thread-6”能够获取锁(即“<0x00000003db200ae0>”)并进一步发展。但是,所有其他 9 个线程都卡住,等待获取此锁。

为什么在调用 Classloader.loadclass() 时线程会被阻塞?

要了解为什么线程在调用 '' 方法时进入 BLOCKED 状态,我们必须查看其源代码。下面是 ClassLoader.loadClass() 方法的源代码摘录。如果您想查看 的完整源代码,可以在此处参考:ClassLoader.loadClass()java.lang.ClassLoader

protected Class<?> loadClass(String name, boolean resolve)        br            throws ClassNotFoundException    br    {        br        synchronized (getClassLoadingLock(name)) {            br            // First, check if the class has already been loaded            br            Class<?> c = findLoadedClass(name);            br            if (c == null) {                br               long t0 = System.nanoTime();                br               try {                    br                   if (parent != null) {                        br                       c = parent.loadClass(name, false);                    br                   } else {                        br                       c = findBootstrapClassOrNull(name);                   br                   }                    br                   :                    br                   :

在源代码突出显示的行中,您将看到“同步”代码块的用法。同步代码块时,只允许一个线程进入该块。在上面的示例中,有 10 个线程尝试同时访问 'ClassLoader.loadClass()'。只允许一个线程进入同步代码块,其余 9 个线程将进入 BLOCKED 状态。

下面是返回对象并进行同步的 '' 方法的源代码。getClassLoadingLock()

protected Object getClassLoadingLock(String className) {   br    Object lock = this;   br    if (parallelLockMap != null) {      br       Object newLock = new Object();      br       lock = parallelLockMap.putIfAbsent(className, newLock);      br       if (lock == null) {     br          lock = newLock;      br       }   br     }   br     return lock; br}

您可以注意到,''方法每次都会为相同的类名返回相同的对象。即,如果类名是'',它每次都会返回相同的对象。因此,所有10个线程都将取回同一个对象。在这个单个对象上,将发生同步。它会将所有线程置于“已阻止”状态。getClassLoadingLock()io.ycrash.DummyObject

如何解决此问题?

这个问题是有原因的,因为''类在每次循环迭代中一次又一次地加载。这会导致线程进入“已阻止”状态。如果我们在应用程序启动期间只能加载类一次,则此问题可能会短路。这可以通过修改代码来实现,如下所示。io.ycrash.DummyObject

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

更改此代码解决了问题。如果您现在看到 “' 已在第 5 行中初始化。与之前每次循环迭代初始化 myClass 的方法不同,现在 myClass 在实例化 Thread 时仅初始化一次。由于代码中的这种变化,“”API 不会被多次调用。因此,它将阻止线程进入阻塞状态。myClassClassLoader.loadClass()

解决 方案

如果您的应用程序还遇到此类加载性能问题,那么以下是解决该问题的潜在解决方案。

  1. 尝试查看是否可以在应用程序启动时而不是运行时调用“”API。ClassLoader.loadClass()
  2. 如果您的应用程序在运行时一次又一次地加载相同的类,则尝试仅加载该类一次。之后,缓存该类并重用它,如上面的示例所示。
  3. 使用故障排除工具,如快速线程,yCrash,...以检测哪个框架或第三方库或代码路径触发了问题。检查框架是否在其最新版本中提供了任何修复程序,如果是,请升级到最新版本。
发布于 2022-08-16 09:07