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()
解决 方案
如果您的应用程序还遇到此类加载性能问题,那么以下是解决该问题的潜在解决方案。
- 尝试查看是否可以在应用程序启动时而不是运行时调用“”API。ClassLoader.loadClass()
- 如果您的应用程序在运行时一次又一次地加载相同的类,则尝试仅加载该类一次。之后,缓存该类并重用它,如上面的示例所示。
- 使用故障排除工具,如快速线程,yCrash,...以检测哪个框架或第三方库或代码路径触发了问题。检查框架是否在其最新版本中提供了任何修复程序,如果是,请升级到最新版本。