JVM(二):深入理解Java类的加载过程

目录

  • 类加载底层原理
  1. .class文件寻址规则(双亲委派)
  2. .class文件校验
  3. 类信息的存储
  4. 类对象的创建
  • forName()与 loadClass()的区别
  • 类并发加载问题

类加载底层原理

从JVM的角度来看,类加载的过程就是查找解析.class文件,并提取其中类信息以某种数据结构存储在方法区中,并在堆内存中创建一个Class对象的过程。细节上又可以分为装载、连接、初始化三个操作,其中装载对应.class文件的寻址及相关编译验证,连接表示把加载后的类数据动态合并到运行中的状态中去(比如分配空间、符号引用的替换等等),初始化表示对Class对象、类变量的初始化等等。加载过程图解如下

从代码层次上来看,基本过程可以分为以下几步
Step1:class文件的寻址(双亲委派)
     Java中class文件的加载是通过类加载器实现的,从本质上来说Java中只有两种类加载器:BootstrapClassLoader(启动类加载器)和用户自定义的类加载器,其中BootstrapClassLoader是JVM内置的加载器,属于JVM实现的一部分。 而其它的比如扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)都可以看成是自定义的类加载器,只是这两者不是由一般程序员编写的,而是由Java API的开发者编写的。
    大多数情况下类加载的策略是:先将类名转换为对应的路径,然后从文件系统的该路径下读取对应的.class文件,比如前面提到的BootstrapClassLoader、ExtClassLoader、AppClassLoader就这样,也可以通过继承ClassLoader或其子类来自定义我们想要类加载器,比如从网络进行加载等等。
    class文件的加载使用了委托模型,即我们常说的双亲委派,除启动类加载器外,每个ClassLoader实例都有一个相关的父加载器成员(这里父不是指继承关系而是依赖关系)。当使用ClassLoader加载某个类时,任务将依次往上优先委托给父加载器,直到父加载器为null,此时将依赖BootstrapClassLoader来加载。如果所有父加载器都没有找到该class文件,则该实例将尝试自己进行装载,如果某个父加载器成功加载了,将依次返回。    
    Java中通过sun.misc.Launcher可以详细的了解Java中相关类加载器的扫描路径等信息,通过ClassLoader类可以详细了解双亲委派模式。
 1-1:Lanucher比较简单,下面是它的相关源码

public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader");
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader");
        }

      //...编幅有限,省略部分
    }


     /*
     * The class loader used for loading installed extensions.
     * 扩展类加载器
     */
    static class ExtClassLoader extends URLClassLoader {

        static {
            ClassLoader.registerAsParallelCapable();
        }

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public ExtClassLoader run() throws IOException {
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }

        void addExtURL(URL url) {
            super.addURL(url);
        }

        /*
         * Creates a new ExtClassLoader for the specified directories.
         */
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");    //扩展类加载器的扫描路径
            File[] dirs;
            if (s != null) {
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }

    /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     * 应用类加载器,等于ClassLoader.getSystemClassLoader()
     */
    static class AppClassLoader extends URLClassLoader {

        static {
            ClassLoader.registerAsParallelCapable();
        }

        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");    //扫描路径
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

            // Note: on bugid 4256530
            // Prior implementations of this doPrivileged() block supplied
            // a rather restrictive ACC via a call to the private method
            // AppClassLoader.getContext(). This proved overly restrictive
            // when loading  classes. Specifically it prevent
            // accessClassInPackage.sun.* grants from being honored.
            //
            return AccessController.doPrivileged(
                new PrivilegedAction() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

从源码中可看出,Launcher采用了单例设计模式,其中启动类加载器、ExtClassLoader和AppClassLoader的扫描路径分别对应系统属性sun.boot.class.path、java.ext.dirs、java.class.path(这里以JDK1.7为例,具体加载路径依赖于具体的实现),而且ExtClassLoader为AppClassLoader的父加载器。静态代码块ClassLoader.registerAsParallelCapable();说明ExtClassLoader和AppClassLoader都支持并发加载(篇幅有限,下一篇博客中会详细解释并发加载原理)。
下方代码简单的打印了各个类加载器的父加载器信息及扫描路径信息

public static void main(String[] args) throws ClassNotFoundException {
		//获取启动类加载器扫描路径
		Launcher launcher = Launcher.getLauncher();
		URL[] urls = launcher.getBootstrapClassPath().getURLs();
		for(URL str : urls){
			System.out.print(str + ";");
		}
//		System.out.println(System.getProperty("sun.boot.class.path"));
		
		//ExtClassLoader父加载器 及 加载路径
		System.out.println(launcher.getClassLoader().getParent().getParent());	//null表示ExtClassLoader无父类加载器,此时优先依赖于启动类加载器加载类
		System.out.println(System.getProperty("sun.boot.class.path"));
	
		//AppClassLoader父加载器 及 加载路径
		System.out.println(launcher.getClassLoader().getParent());	//sun.misc.Launcher$ExtClassLoader@5736ab79
		System.out.println(System.getProperty("java.class.path"));	

	}

 1-2:双亲委派源码分析(构造器源码+加载源码)
ClassLoader构造器源码,由于篇幅有限,这里去掉了相关注释

    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();    //类名与对应锁对象的映射Map
            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;
        }
    }
   
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

     /**
     * 默认构造器:未指定父加载器,默认使用系统类加载器
     */
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

parent作为ClassLoader的成员而存在(并非父类),从源码中可以看出当未指定parent时将默认为系统类加载器,即Launcher中的ClassLoader(AppClassLoader)。

 public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();    //初始化系统类加载器
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader ccl = getCallerClassLoader();
            if (ccl != null && ccl != scl && !scl.isAncestor(ccl)) {
                sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
        return scl;
    }

    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();    //上文中的Launcher,为单例
            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader();    //通过laucher获取类加载器,即AppClassLoader
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }

ClassLoader加载相关源码:

 protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //同步控制:开启并发加载的情况下,锁对类对应的锁对象,否则为ClassLoader对象本身
        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;
        }
    }

    /**
     * 获取类名对应的锁对象
     *
     * @see #loadClass(String, boolean)
     *
     * @since  1.7
     */
    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;
    }

ClassLoader并发加载是通过一个ConcurrentHashMap实现的,Key为类名,对应的Value为一个new Object(),所以它可以同时加载多个类,但同一个类重复加载时则可以锁住。通过代码
static{
        registerAsParallelCapable();   
    }可以启用并发加载。
篇幅有限,关于自定义ClassLoader及并发加载参考另一篇博客https://blog.csdn.net/w1673492580/article/details/81912344

Step2:.class文件的校验
     
字节码文件本质上只是一个字节序列,这个字节序列并不一定是经过Javac编译后产生的结果,比如说完全可以创建一个后缀为.class的文件,或者修改其中已有内容等等,所以从安全的角度出发,JVM实现一般都会提供一个.class文件校验器。class文件校验也是类加载过程中的第一步,这一步基本上会对.class文件进行了很全面的校验,比如结构是否正确(如以魔数0xCAFEBABE开头)、语义是否合理(如方法签名、不能多继承等信息)、字节码流和栈以及栈桢的检查(比如必须局部变量需先声明后使用、不能重名、值与类型必须一致等)等等。下面是一个resolveInClass()方法对应的字节码流

 // Method descriptor #128 (Ljava/lang/Class;Ljava/lang/reflect/Type;)Ljava/lang/reflect/Type;
  // Signature: (Ljava/lang/Class<*>;Ljava/lang/reflect/Type;)Ljava/lang/reflect/Type;
  // Stack: 2, Locals: 2
  public static java.lang.reflect.Type resolveInClass(java.lang.Class arg0, java.lang.reflect.Type arg1);
    0  aload_0 [arg0]
    1  invokestatic com.sun.beans.TypeResolver.getActualType(java.lang.Class) : java.lang.reflect.Type [179]
    4  aload_1 [arg1]
    5  invokestatic com.sun.beans.TypeResolver.resolve(java.lang.reflect.Type, java.lang.reflect.Type) : java.lang.reflect.Type [182]
    8  areturn
      Line numbers:
        [pc: 0, line: 81]

Step3:类信息的存储
       在.class文件校验通过之后,JVM会解析并抽取其中类相关的的二进制数据,以内部结构存储在方法区中,到这一步类信息已经加载在方法区中。类信息具体的存储结构依赖于不同的JVM实现,关于方法区的详细信息可以参考前一篇博客:

Step4:Class对象的创建
        把类信息存储在方法区中之后,JVM就可以根据类信息在堆中分配合适的大小空间,创建Class对象(JVM虽然可以根据类信息知道对象的大小,但实际分配的大小取决于JVM的具体实现)。这里的创建包括为类变量分配置内在,并依次执行所有成员的默认初始化、显示初始化、构造器初始化等等操作,其中创建的Class对象中维护了一个指向类信息的指针及对应ClassLoader对象的引用,所以在Java中我们才可以用class.getField(name)获取类相关信息、getClassLoader获取类加载器。关于具体的对象创建流程参考:https://mp.csdn.net/postedit/81838835

forName()与loadeClass的区别
      上文说过,类加载的过程可以细分为装载、连接、初始化,Java中Class.forName()和ClassLoader.loadeClass()分别如下
public static Class forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException //initialize表示加载过程中是否初始化

protected Class loadClass(String name,boolean resolve) //resolve表示是否连接,并不一定会初始化,
下面是两者相关的测试代码

public class CacheClassLoaderTest {

	public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		//true表示执行初始化操作,打印输出"----clinit",为false则无打印
		Class.forName("com.tvl.cache.config.CacheConfigTest",false,Launcher.getLauncher().getClassLoader());
		
		//加载不会执行解析步取,经测试也没有执行初始化操作,无打印信息。直到运行调用newInstance()才真正执行类初始化
		Class clazz = Launcher.getLauncher().getClassLoader().loadClass("com.tvl.cache.config.CacheConfigTest2");
		clazz.newInstance();	
		
		//使用自定义的类加载器加载,即使设置resolve为true,同样没有执行初始化操作,无打印信息
		final CacheClassLoader loader = new CacheClassLoader("D:\\feiniu\\zst\\tvlCache\\target\\classes\\");
		loader.loadClass("com.tvl.cache.config.CacheConfigTest3");	//内部仅调用 return super.loadClass(name,true);
	}
	
}

class CacheConfigTest{
	static{
		System.out.println("----clinit");
	}
}
class CacheConfigTest2{
	static{
		System.out.println("----clinit2");
	}
}
class CacheConfigTest3{
	static{
		System.out.println("----clinit3");
	}
}

测试结果说明,默认情况下forName() 加载的类,在返回Class实例之前已经执行了类初始化;而loadClass ( ) 没有执行,而是在运行调用时才真正执行。通常情况下都建议采用forName()进行加载,比如常见的加载JDBC驱动Class.forName(“xxx.JDBC.."),当然ClassLoader相对于forName()来说也有优势,比如使用自定义的ClassLoader可以从网络、DB等很多地方加载类。

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