JVM---类加载机制

说明:类是在运行期间第一次使用时动态加载的,而非一次性加载(因为一次性加载占用内存较大)。

一、类的生命周期

七阶段:加载(Loading)-->验证(Verification)-->准备(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->卸载(Unloading)

JVM---类加载机制_第1张图片

二、类的加载过程

说明:类加载过程包含加载、验证、准备、解析和初始化5个阶段。

1、加载(这里的加载是指类加载的加载阶段)

加载过程完成了三件事:

1)通过一个类的全限定名来获取定义此类的二进制字节流

2)将这个二进制字节流所代表的静态存储结构转化为方法区的运行时存储结构

3)在内存中生成一个代表这个类的Class对象,作为方法区对这个类的各种数据访问的访问入口。

其中,二进制字节流可以从以下方式中获取:

1)从ZIP包读取,成为JAR、EAR、WAR格式的基础;

2)从网络中获取,最典型的应用是Applet;

3)运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流;

4)由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

2、验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3、准备

1)类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。

2)实例变量不会在这个阶段分配内存,它将会在对象实例化时随着对象一起分配在堆中。

注意:实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

eg:public static int value = 123; 初始值一般为0,该类变量 value 被初始化为 0 而不是123;

public static final int value = 123; 若类变量是常量,则会按照表达式来初始化,而不是赋值为 0,所以该value会被初始化为123.

4、解析

将常量池的符号引用替换为直接引用的过程。

注意:解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持java的动态绑定。

5、初始化

1)初始化阶段才真正开始执行类中定义的Java程序代码。初始化阶段即虚拟机执行类构造器()方法的过程。

2)在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

3)() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

public class Test {
    static {
        i = 0; //给变量赋值可以正常编译通过
        System.out.println(i); //此时编译器会提示“非法向前引用”
    }
    static int i = 1;
}

由于父类的() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
    System.out.println(Sub.B);  // 2
}

4)接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成() 方法。但接口与类不同的是,执行接口的() 方法不需要先执行父接口的() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的() 方法。

5)虚拟机会保证一个类的() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的()方法,其他线程都会阻塞等待,直到活动线程执行()方法完毕。如果在一个类的() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

三、类初始化时机

1、主动引用

虚拟机规范中并没有强制约束合适进行记载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则必须先出发其初始化。最常见的生成这四条指令的场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法的时候。

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先去触发其父类的初始化。

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

2、被动引用

以上五种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

1)通过子类引用父类的静态字段,不会导致子类初始化。

System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义

2)通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承Object的子类,其中包含了数组的属性和方法。

SuperClass[] sca = new SuperClass[10];

3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

System.out.println(ConstClass.HELLOWORLD);

四、类与类加载器

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

这里的相等,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回值结果为true,也包括使用instanceof关键字做对象所属关系判定结构为true。

五、类加载器分类

1、从Java虚拟机角度来分:

1)启动类加载器(Bootstrap ClassLoader),这个类加载器用C++实现,是虚拟机自身的一部分;

2)所有其他类的加载器,这些类由Java实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

2、从Java开发人员角度来分:

1)启动类加载器(Bootstrap ClassLoader):此类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用null代替即可。

2)扩展类加载器(Extension ClassLoader):这个类加载器由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将\lib\ext或者被java.ext.dir系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。

3)应用程序类加载器(Application ClassLoader):这个类加载器是由AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是ClassLoader 中的getSystemClassLoader()方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这就是程序中默认的累加器。

六、双亲委派模型

应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

下图展示的是类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般是通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。

JVM---类加载机制_第2张图片

1、工作过程

一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才会尝试自己加载。

2、优点

使得Java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到同一。

例如:java.lang.Object存放在rt.jar中,如果编写另外一个java.lang.Object的类并放到ClassPath中,程序可以编译通过。由于双亲委派模型的存在,所以在rt.jar中的Object比在ClassPath中的Object优先级更高,这是因为rt.jar中的Object使用的是启动类加载器,而ClassPath中的Object使用的是应用程序类加载器。rt.jar中的Object优先级更高,那么程序中所有的Object都是这个Object。

3、实现

以下是抽象类 java.lang.ClassLoader的代码片段,其中的loadClass()方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时,抛出ClassNotFoundException,此时尝试自己去加载。

public abstract class ClassLoader {
    private final ClassLoader parent;
    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;
        }
    }
    protected Class findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

 

七、自定义类加载器实现

FileSystemClassLoader是自定义类加载器,继承自java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节码文件(.class文件),然后读取该文件的内容,最后通过defineClass()方法来把这些字节代码转换成java.lang.Class类的实例。

java.lang.ClassLoader的loadClass()实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写findClass()方法。

public class FileSystemClassLoader extends ClassLoader{
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if(classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar +
                className.replace('.', File.separatorChar) + ".class";
    }
}

 

你可能感兴趣的:(JVM---类加载机制)