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
