java class文件的加载

版权声明:转载请标明来源 https://blog.csdn.net/u010652576/article/details/78327985

java class文件加载过程:jvm把描述的数据从class文件加载(loading)到内存(java方法区)中,中间对数据进行校验(verification)、转换解析(resolution)和初始化(initialization),最终形成可以被jvm直接使用的Java类,这就是class文件的加载。

例如:Student.class,通过class文件的加载,就可以直接通过newInstance创建对象,供jvm使用。

同时jvm把class文件加载到内存中,在jvm中就形成一份描述Class结构的元信息对象(Class对象,存在java堆中),通过该元信息对象就可以获知Class的结构信息,例如:构造函数、属性、方法等,Java也允许用户借由这个元信息对象间接调用Class对象的功能。

例如:
1 Class.forName(“classLoader.ClassLoaderTest”).getClassName();//获取class的名称
2 Class.forName(“classLoader.ClassLoaderTest”).getClassLoader();//获取类加载器
3 Class.forName(“classLoader.ClassLoaderTest”).getMethod();//获取方法
4 User.class.getClassLoader().loadClass(“classLoader.ClassLoaderTest”).getField();//获取属性

class的生命周期

![class的生命周期](https://img-blog.csdn.net/20171024145920697?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDY1MjU3Ng==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段,这就有可能出现对象虽然不为null,但是仍存在部分字段没有初始化完全,因此单利模式的double-check-lock的对象需要加volatile修饰。

加载(loading)

主要做了3件事:
1 通过一个类的全限定名来获取其定义的二进制字节流。就是常见的Class.forName(className)中的className,一般都是包名类名。
2 把该字节流所代表的静态存储结构转化为方法取的运行数据结构,此时相关的class类的相关信息就存储到方法区。
3 根据class文件,在java堆中创建一个该class文件对应的Class对象,作为方法取中数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载,主要是通过自定义类加载器进行控制。
关于类加载器如何查找到class文件以及如何加载,请参考:

连接(linking)

连接分为以下几步:验证、准备、解析。

验证:验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备:为类的静态变量分配内存,并将其初始化为默认值。,注意仅对静态变量进行内存分配和初始化值,这个值是系统默认的初始化值,例如:0,0L,”“,null等,而不是代码中赋的值。
示例:public static int value = 3;
此时 value的值就是系统默认0,而不是代码中赋的值3,3需要到初始化的时候进行复制。当然如果被修饰为final static int value = 3,那么准备结束后,value就是3了,static final常量在编译期就将其结果放入了调用它的类的常量池中。
还需要注意如下几点:
1 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。例如:方法内定义 int i;会提示initialize variable 初始化变量。
2 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
3 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
4 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
解析:把类中的符号引用转换为直接引用。解析阶段是虚拟机将常量池内符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化(initialization)

为类的静态变量赋予正确的初始值,JVM负责对类进行初始化。例如:static int a = 3;解析后a的值为0,初始化之后a的值为3。

初始化的方式主要有2种:
1 指定初始值,例如:static int a = 3;直接声明。
2 通过静态代码块,进行赋值。
例如:static int a;static{ a =3;},初始化之后a的值为3。相当于static代码块就是给static变量初始化值使用,并且只执行一次。

jvm的初始化步骤:
   1 该类是否被加载过,如果没有,就进行loading,linking后,再初始化。
   2 如果该类的直接父类没有进行初始化,就先初始化直接父类。
   3 如果类中有初始化语句(static代码块,或者赋值语句),一次执行初始化语句。
   记住:优先初始化该类的直接父类。
jvm初始化的时机:
    1 直接创建类的实例,通过new的方式,这是会类初始化,当然除了初始化,还有后续的实例化。
    2 访问某个类\接口的静态变量(get/set值),此时会直接初始化该静态变量所在的类。注意:即使通过子类访问父类的静态变量,那么也只会初始化父类。      
package classLoader.dynamic;

public class Parent {
    static String name;
    static{
        System.out.println("parent init--befor:"+name);
        name="123";
        System.out.println("parent init--after:"+name);
    }
    public static void main(String[] args) {
        System.out.println("print:"+Son.name);
    }
}

class Son extends Parent{
    static{
        System.out.println("son init");
    }

}

输出结果-------
parent init--befor:null
parent init--after:123
print:123

并没有执行Son中的static代码块,同时需要注意:main方法在Parent中,如果main方法在Son中,Son的static就会执行,这是因为Son是main方法的入口,需要初始化:输出如下:
parent init--befor:null
parent init--after:123
son init
print:123

3 调用类的静态方法。
4 反射(如Class.forName(“com.shengsiyuan.Test”))。
5 初始化某个类的子类,则其父类也会被初始化,这是隐式的初始化。
6 Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类。

类加载器

把class文件,加载进jvm内存中,需要通过类加载器,jvm的类加载器分为4种(从加载内容的位置分):

1 启动类加载器(bootstrap classloader):jvm核心的类加载器,是虚拟机自身的类加载器,无法被Java程序直接引用。负责加载%JAVA_HOME%\jre\lib下的所有jar包,例如:String类的核心jar包,就是有bootstrap classLoader加载的。
2 扩展类加载器(extention classLoader):继承自ClassLoader对象,需要由bootstrap classLoader加载后,才能加载其他类,同时该类的父亲就是bootstrap classLoader,负责加载:%JAVA_HOME%\jre\lib\ext可以被Java程序调用,用来加载其他class,注意:两者的父子关系并不是通过继承实现的,而是通过组合实现的,即通过类中的parent属性获取父类。
3 自定义类加载器(custom classLoader):如果上述类加载器不满足需要,可以自定义classLoader,从指定的位置加载class,同时可以进行一些加载前和加载后的处理,例如:加载前进行解密、不进行class文件的缓存等。自定义类加载器需要继承自ClassLoader,或者继承自一些系统提供给的classLoader,例如:URLClassLoader,只需重写里面的findClass方法即可。

一般自定义classLoader有如下应用:
    (1)在执行非置信代码之前,自动验证数字签名,比如:那么为了安全需要对class文件加密,那么就可以自定义classLoader进行解密和转化。
    (2) 动态地创建符合用户特定需要的定制化构建类。这个主要是用来动态加载class文件,保证文件的修改可以立刻生效。
    (3) 从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制

1 全盘负责制:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。例如:application classLoader加载User class文件,那么其中User通过继承的父类、实现的接口类、导入的jar包等,加载都是有按品牌里餐厅 classLoader负责。
2 父类委托:当前加载器会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类,这就是类加载的双亲委派。通过父类委托,当appliation加载User class文件时,会优先询问父类是否加载,如果父类没有加载,那么application classLoader就会尝试在classPath路径下加载改类,这同样适用于User继承的父类、实现的接口类等。

类加载机制

1 命令行启动应用时候由JVM初始化加载。
2 通过Class.forName()方法动态加载,除了加载进内存,同时会进行初始化。
3 通过ClassLoader.loadClass()方法动态加载,这种加载只是把class文件加载进内存,并不进行初始化。

示例:

package classLoader;

public class ClassInit {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("classLoader.Son");//方法一
        ClassLoader.getSystemClassLoader().loadClass("classLoader.Son");//方法二
    }
}

class Son{
    static String name;
    static{
        System.out.println("init-before:"+name);
        name="123";
        System.out.println("init-after:"+name);
    }
}



-------输出结果:
方法一:
    init-before:null
    init-after:123
方法二:

方法一与方法二是互斥的。其中方法二并不会初始化,因此没有输出。
当然Class.forName(),也可以通过参数配置,不进行初始化。

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

示例:
1 当用户加载一个class文件,获取系统的class loader,默认的是Application classLoader,此时Application classLoader先去缓存中,查看该class是否被jvm记录了,有记录就返回,如果没有就请求Extention calssLoader,如果extention classLoader没有加载成功,那么Application classLoader就会去classpath路径下加载,仍为加载成功抛出ClassNotFindException。
2 Extention classLoader 也不会直接加载class文件,而是交给父加载器bootstrap classLoader去加载,如果bootstrap classLoader没有加载成功,那么extention classLoader就会去%JAVA_HOME%\jre\lib\ext下面查找,成功就加载,否则就返回null,交给Application classLoader加载。
3 因为bootstrap classLoader加载器没有父加载器,因此bootstrap classLoader直接在%JAVA_HOME%\jre\lib下,检索该class文件,如果没有就返回null,交给extention classLoader去加载。

    双亲委派模型意义:
    -系统类防止内存中出现多份同样的字节码
    -保证Java程序安全稳定运行

ClassLoader.java的loadClass源码

public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }

    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判断该类型是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
                try {
                    if (parent != null) {
                         //如果存在父类加载器,就委派给父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

自定义类加载器

通常我们使用系统的类加载器(默认为Application classLoader)。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法即可。为什么只需要重写findClass方法即可?

1 首先从ClassLoader的构造方法中看:

//无参构造函数:调用了有参构造函数,其中getSystemClassLoader()方法,获取系统的classLoader作为ClassLoader的parent,而通过ClassLoader.getSystemClassLoader()方法,获取的类加载器就是Application classLoader,因此不需要考虑双亲委派加载模型了。

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
//有参构造函数,指定classLoader作为ClassLoader的父加载器。
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<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

2 从ClassLoader的loadClass方法中,发现当Bootstrap classLoader、ExtentionClassLoader、Application classLoader、ClassLoader都为成功加载class文件后,会抛出ClassNotFindException,此时catch后,调用了findClass,因此我们只需要重新findClass,把我们的class文件返回即可。

从ClassLoader提供的范例看

//继承ClassLoader
 class NetworkClassLoader extends ClassLoader {
    String host;
    int port;
    //重写findCladd
    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    //加载Class文件,生成字节数组即可,因此可以在loadClassData()方法中,添加自定义方法即可,例如解密,class文件的获取等。
    private byte[] loadClassData(String name) {
        // load the class data from the connection
         . . .
    }
}

注意:
    1 如果是自定义classLoader,尽量不要重写ClassLoader的loadClass方法,因为这会破坏双亲委派。
    2 如果是加载本地class,该class文件不要放在classpath中,因为双亲委派,application classLoader会提前加载。
    3 findClass(String namee)方法的name尽量按照全路径(包名+类名),因为defineClass方法是按照这种方式处理,同时全路径也能解决class文件的缓存的唯一性。
说明:
    1 如果想更好的了解自定义classLoader,可以参考URLClassLoader,根据url地址,加载指定名称的class文件。
    2 class文件加载进jvm中,判断Class对象的唯一性,就依靠,loadClass(String name)和使用的类加载器,两者全部相同时,才认为是同一个Class,例如:com.classLoader.A.class和com.classLoader.B.class就不是同一个Class对象;由Application classLoader加载的com.classLoader.A.class和Extention classLoader加载的com.classLoader.A.class同样不是同一个Class对象。

参考文章:
1 http://www.cnblogs.com/ityouknow/p/5603287.html 文章主要是参考这篇文章写的。
2 http://www.importnew.com/18548.html
3 http://blog.csdn.net/u013256816/article/details/50837863
2和3的参考文章,其中关于子父类的static代码块、构造代码块、代码块的执行顺序有很好的解说。

猜你喜欢

转载自blog.csdn.net/u010652576/article/details/78327985