JVM(三):自定义ClassLoader 及 类并发加载问题

问题:
  目前大部分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 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:启用并发容器
	}

 

你可能感兴趣的:(Java高级,JVM)