深入理解Java虚拟机——虚拟机类加载机制

类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载 七个阶段。其中验证、准备、解析三个部分统称为连接

深入理解Java虚拟机——虚拟机类加载机制_第1张图片

加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类加载过程必须按照这中顺序开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,注意,这里是按部就班地“开始”,并不是“进行”或者“完成”,强调这一点是因为这些阶段通常都是相互交叉地混合式进行的,通常会在一个阶段执行过程中调用、激活另一个阶段。

虚拟机严格规定了有且只有5种情况必须立即对类进行“初始化”:
1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先出发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先出发其初始化。
3.当初始化一个类的时候,如果发现父类还没进行过初始化,则需要先触发父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
实例:

public class ParentClass {
    static {
        System.out.println("父类静态代码块!");
    }
    public static int value = 1;
    public static final String HELLO = "hello";
}

public class ChildClass extends ParentClass {
    static {
        System.out.println("子类静态代码块!");
    }
}

//被动引用方式1
public class Entrance1 {
    public static void main(String[] args) {
        System.out.println(ChildClass.value);
    }
}
//被动引用方式2
public class Entrance2 {
    public static void main(String[] args) {
        ParentClass[] parentClasses = new ParentClass[10];
    }
}
//被动引用方式3
public class Entrance3 {
    public static void main(String[] args) {
        System.out.println(ParentClass.HELLO);
    }
}

结果
Entrance1 通过子类引用父类的静态字段,子类不会初始化,父类初始化;
Entrance2 通过数组定义引用类,该类不会初始化;
Entrance3 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用定义常量的类,因此不会触发类的初始化;

类加载的过程

加载
在加载阶段,虚拟机需要完成以下三件事件:

  1. 通过一个类的全限定名(如:java.lang.String)来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(1.8以后为元空间);
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区(元空间)这个类的各种数据的访问入口。

 加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中(元空间)。加载阶段与连接阶段的部分内容时交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

验证
验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
验证阶段大致上会完成下面4个阶段的验证动作:文件格式验证;元数据验证;字节码验证;符号引用验证。

准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区(元空间)中进行分配。这里的内存分配仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配到Java堆中。设置初始值指的时零值,比如 public static int value = 123; 准备阶段后,value的值是0而不是123,赋值123的动作将在初始化阶段进行。特殊情况:public static final int value = 123;这种情况在准备阶段就会赋值123。

解析
解析阶段是虚拟机将常量池内的符号引用给替换为直接引用的过程
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现地内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例翻译出来的直接引用一般不相同。如果有了直接引用,那引用的目标必定是已经在内存中存在。

初始化
类初始化阶段时类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序指定的主观计划去初始化类变量和其他资源。初始化阶段是执行类构造器的()方法的过程。

类加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机的一部分;另一种就是所有其他的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

  • 启动类加载器:这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如:rt.jar,名字不符合的类库即使放在lib目录中也不会加载)类库加载到虚拟机内存中。
  • 扩展类加载器:这个加载器负责加载\lib\ext目录中,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称他为系统类加载器。它负责加载用户路径上(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型

深入理解Java虚拟机——虚拟机类加载机制_第2张图片

双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器都要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个java.lang.Object类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,应用程序将会一片混乱。
实现双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法中:

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;
        }
    }

先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。若父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

本文内容参考《深入理解Java虚拟机 JVM高级特性与最佳实践》-周志明

 

 

 

 

 

 

你可能感兴趣的:(JVM)