问题:
目前大部分JVM实现都采用延迟加载的策略,在运行时,当需要用到某个类时才会去真正装载该类。当JVM加载某个类时,会提取出其中的类型信息存储在方法区中,类的静态变量也同样存在该方法区中,虽然JVM内部以什么样的数据结构来存储类型信息,依赖于JVM的具体实现,但从存储的信息基本是一样的,比如类的基本信息(完全限定名称、父类信息、加载器信息等等)、常量池、字段信息、方法信息(签名、字节码流、操作数栈等信息)等等。
关于把类加载到方法区,有个很普遍的常识:“类只会被同一ClassLoader加载一次”,但对于JVM来说它的方法区和堆,是 JVM中所有线程共享的,如何保证只加载一次? 如果以ClassLoader对象为锁来保证,当存在大量类需要加载时,这种方式必将严重影响性能,那如何并发加载?
源码分析:下面是ClassLoader中的相关源码,篇幅有限,所以抽取了相关方法放在一起(基于jdk 1.7)
/**
* 构造函数
*
*/
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) { //注册了当前类对象
parallelLockMap = new ConcurrentHashMap<>(); //使用并发容器
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
/**
* 注册当前ClassLoader
*
*/
protected static boolean registerAsParallelCapable() {
Class extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
return ParallelLoaders.register(callerClass);
}
/**
* 加载类
*
*/
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);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 加载类:获取锁对象
*
*/
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;
}
从源码中可以看出,当load一个类时,默认情况下锁是当前的ClassLoader对象,这意味着”相同的ClassLoader"在并发加载类时(即使是不同类),同时只会有一个线程在加载,其它线程都是阻塞。想象一下一个的场景:假设程序中有1W个类,1W个用户同时来访问,每个访问需要加载的类都不同(但由同一ClassLoader加载),这种情况下,以ClassLoader对象本身为锁的方式将会严重影响性能。
Debug源码后发现,通过registerAsParallelCapable(); 可以让ClassLoader使用并发容器。这种情况下,当ClassLoader加载类时,如果该类是第一次加载,则会以该类的完全限定名称作为Key,一个new Object()对象为Value,存入一个ConcurrentHashMap的中。并以该object对象为锁进行同步控制。同一时间如果有其它线程再次请求加载该类时,则取出map中的对象object,发现该对象已被占用,则阻塞。也就是说ClassLoader的并发加载通过一个ConcurrentHashMap实现的。
测试验证:下面分别是这种两情况的测试代码:首先先自定义了一个类加载器,用于加载本地磁盘中的.class文件。
/**
* 自定义类加载器:加载本地磁盘中的.class文件
*
* @description
* @author shite.zhu
*
*/
public class CacheClassLoader extends ClassLoader {
private String dir;
private static final String SUFFIX = ".class";
static{
// registerAsParallelCapable(); //TODO:启用并发容器
}
public CacheClassLoader(String dir) {
// 默认情况下自定义的ClassLoader双亲是AppClassLoader
// 由于测试的CacheConfig.class文件在应用中已经存在,为了不让AppClassLoader加载,所以这里设置双亲为null
// 设置为null,则只会尝试从启动类加载器进行加载,无法加载的情况下就会尝试用本加载器进行加载
super(null);
this.dir = dir;
}
@Override
protected Class> findClass(String name) {
try {
// 0、文件对应磁盘路径
String newName = dir + name.replaceAll("\\.", Matcher.quoteReplacement(File.separator)) + SUFFIX;
// 1、获取文件字节数组
byte[] classByte = loadClassData(newName);
// 2、生成Class对象
Class> cl = defineClass(name, classByte, 0, classByte.length);
// 打印当前加载线程
System.out.println("当前加载线程:" + Thread.currentThread().getName() + ",加载器:" + cl.getClassLoader());
return cl;
} catch (Exception e) {
// test...不做任何事
e.printStackTrace();
throw new ClassFormatError(e.getMessage());
}
}
/**
* 读取.class文件,返回字节数组
*
* @param name
* 文件路径
* @return 字节数组
* @throws Exception
*/
private byte[] loadClassData(String name) throws Exception {
try (final java.io.FileInputStream fis = new java.io.FileInputStream(name)) {
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
return bytes;
}
}
}
测试场景01:默认情况下,同一ClassLoader并发加载多个类,结果说明这种情况下锁对象是ClassLoader本身。
/**
* @description
* @author shite.zhu
*
*/
public class CacheClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
//0、创建loader对象
final CacheClassLoader loader = new CacheClassLoader("D:\\feiniu\\zst\\tvlCache\\target\\classes\\");
//1、多线程调用同一loader对象,加载不同类CacheCofnig
int i = 0;
while(i++ < 5){
final int j = i - 1;
new Thread(new Runnable() {
@Override
public void run() {
try {
loader.loadClass("com.tvl.cache.config.CacheConfig" + j);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}).start();;
}
}
}
测试场景02:开启同步容器后,同一ClassLoader并发加载多个类,结果说明这种情况下锁对象是类对应的new Object()对象。
static{
registerAsParallelCapable(); //TODO:启用并发容器
}