Java类加载

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被称作虚拟机的类加载机制。与那些需要在编译时进行链接的语言不同,java的加载,连接,初始化都是在程序运行时完成的
Java类加载_第1张图片
为了支持java的动态绑定特性,解析操作并不需要严格按照图中位置执行,它可以在初始化之后再开始。

加载

加载是整个类加载过程的第一个阶段,在加载阶段,java虚拟机需要完成以下三件事:

  1. 通过一个类的全限定名获得定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口
    对这三点的要求并不是特别具体,留给实现者的空间是很大的,比如第一条,java虚拟机有意的将这个动作放在虚拟机外部去实现,以便让应用程序自己去决定如何获取所需类。它并没有规定必须从某个Class文件中获取,甚至没有说明从哪里获取,怎么获取。因此,除了从Class文件获取外,还可以从jar包,war包,动态代理,JSP文件,甚至数据库、网络中获取。加载阶段是整个类加载过程中开发人员可控性最强的阶段,加载阶段可以使用java虚拟机内置的类加载器,也可以使用用户自定义的类加载器。类加载器负责完成加载阶段的工作,即上述的类加载需要完成的三件事。我们通过jdk源码来看类加载器详细的加载流程

通过一个类的全限定名获得定义此类的二进制字节流
这一步是通过类加载器的loadClass方法实现的。

/**
     * Loads the class with the specified binary name.
     * This method searches for classes in the same manner as the {@link
     * #loadClass(String, boolean)} method.  It is invoked by the Java virtual
     * machine to resolve class references.  Invoking this method is equivalent
     * to invoking {@link #loadClass(String, boolean) loadClass(name,
     * false)}.
     *
     * @param  name
     *         The binary name of the class
     *
     * @return  The resulting Class object
     *
     * @throws  ClassNotFoundException
     *          If the class was not found
     */
public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
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;
        }
    }

name就是类的全限定名,而resolve则表示是否要在加载阶段执行连接(即验证、准备、解析)操作。
首先,Class c = findLoadedClass(name);查找该类是否已被加载过,如果加载过,直接返回,否则,
会触发双亲委派模型,即先交给父类加载器进行加载,当父类加载器无法定位到这个类,再由自己进行加载。自定义的类加载器通过重写findClass(name);来实现自己的加载逻辑。我们需要找到一个子类加载器的findClass方法来一窥具体的类加载过程,下面是AppletClassLoader的findClass实现

 protected Class findClass(String name) throws ClassNotFoundException {

        int index = name.indexOf(";");
        String cookie = "";
        if(index != -1) {
                cookie = name.substring(index, name.length());
                name = name.substring(0, index);
        }

        // check loaded JAR files
        try {
            return super.findClass(name);
        } catch (ClassNotFoundException e) {
        }

        // Otherwise, try loading the class from the code base URL

        // 4668479: Option to turn off codebase lookup in AppletClassLoader
        // during resource requests. [stanley.ho]
        if (codebaseLookup == false)
            throw new ClassNotFoundException(name);

//      final String path = name.replace('.', '/').concat(".class").concat(cookie);
        String encodedName = ParseUtil.encodePath(name.replace('.', '/'), false);
        final String path = (new StringBuffer(encodedName)).append(".class").append(cookie).toString();
        try {
            byte[] b = (byte[]) AccessController.doPrivileged(
                               new PrivilegedExceptionAction() {
                public Object run() throws IOException {
                   try {
                        URL finalURL = new URL(base, path);

                        // Make sure the codebase won't be modified
                        if (base.getProtocol().equals(finalURL.getProtocol()) &&
                            base.getHost().equals(finalURL.getHost()) &&
                            base.getPort() == finalURL.getPort()) {
                            return getBytes(finalURL);
                        }
                        else {
                            return null;
                        }
                    } catch (Exception e) {
                        return null;
                    }
                }
            }, acc);

            if (b != null) {
                return defineClass(name, b, 0, b.length, codesource);
            } else {
                throw new ClassNotFoundException(name);
            }
        } catch (PrivilegedActionException e) {
            throw new ClassNotFoundException(name, e.getException());
        }
    }

可以看到,它的确是用自己的方式找到了目标类的二进制字节流,这完成了第一个任务。随后,它将字节流传入了defineClass方法

/**
converts an array of bytes into an instance of class Class, with an optional CodeSource. Before the class can be used it must be resolved.
If a non-null CodeSource is supplied a ProtectionDomain is constructed and associated with the class being defined.

Params:
name – the expected name of the class, or null if not known, using '.' and not '/' as the separator and without a trailing ".class" suffix.
b – the bytes that make up the class data. The bytes in positions off through off+len-1 should have the format of a valid class file as defined by The Java™ Virtual Machine Specification.
off – the start offset in b of the class data
len – the length of the class data
cs – the associated CodeSource, or null if none
Returns:
the Class object created from the data, and optional CodeSource.
Throws:
ClassFormatError – if the data did not contain a valid class
IndexOutOfBoundsException – if either off or len is negative, or if off+len is greater than b.length.
SecurityException – if an attempt is made to add this class to a package that contains classes that were signed by a different set of certificates than this class, or if the class name begins with "java.".
**/
protected final Class<?> defineClass(String name,
                                         byte[] b, int off, int len,
                                         CodeSource cs)
    {
        return defineClass(name, b, off, len, getProtectionDomain(cs));
    }

注释解释到这个方法的作用就是将字节流转化成一个Class的实例对象,即我们加载阶段的2、3部分。具体的转变过程是JNI调用native 方法执行

private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);

看来逻辑在HotSpot虚拟机源码中,也就是C++实现的了。但通过阅读Java部分的源码,我们也了解了类加载器的作用和工作流程,包括双亲委派机制。

验证

验证是为了确保加载阶段引入的字节流符合虚拟机的规范要求,如果不检查输入的字节流,很可能会因为载入了有错误或者恶意企图的字节流而导致整个系统崩溃

准备

准备阶段为类中定义的变量(被static修饰的变量)分配内存并设置变量初始值。

解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定是已经加载到虚拟机内存中的内容,各种虚拟机实现的内存布局可能不同,但它们能接受的符号引用必须是一致的。而直接引用是直接指向目标的指针,相对偏移量,或者是一个句柄。直接引用与内存布局直接相关。如果有了直接引用,那么引用的目标在虚拟机内存中一定存在。解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符

初始化

初始化阶段是类加载的最后阶段,在前面的几个阶段中,除了加载阶段可以由用户自定义的类加载器进行外,其他都是在虚拟机内部进行的,直到这一步才将控制权转交给用户。初始化阶段就是类构造器 ()方法执行的过程 ,该方法是由编译器 自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。他与类的构造函数 ()方法不同,不需要显式调用父类的构造器,Java虚拟机会保证子类的clinit方法执行前父类的已经执行

你可能感兴趣的:(java,jvm,开发语言)