第七章 虚拟机类加载机制

虚拟机把描述类的数据从class文件加载到内存中,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类从加载到虚拟机内存开始,到卸载出内存,一共经历了7个步骤:

  • 1.加载

  • 2.验证

  • 3.准备

  • 4.解析

  • 5.初始化

  • 6.使用

  • 7.卸载

对于初始化,Java虚拟机规范做了严格的限制,有且只有四种情况必须立即对类进行“初始化”

  • 遇到new,get static, put static, invoke static 这四条指令码时,如果类没有进行初始化,必须触发初始化。new 肯定是新建对象,而get/put static 是读取或者设置一个类的静态字段

  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,必须触发初始化

  • 当初始化一个类时,如果其父类还没有初始化,则触发初始化

  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类)

类加载的过程

1.加载

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

+ 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(保存类的基本信息,名字,实现的接口等)

+ 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口(生成的这个Class对象是该类所有对象的模板。然后该类的所有实例想获取类信息的时候,就通过这个代表该类的唯一的Class对象来访问方法区的信息)

2. 验证

由于Java的开放,字节流有五花八门的来源,如果有人写了恶意的代码,那么在执行的时候就会让程序或者JVM崩溃。

+ 1.文件格式验证:字节流是否符合.class文件格式规范,能否被当前版本虚拟机处理(比如开头是CAFEBABE)。该阶段的主要目的是保证输入的字节流能正确地解析并保存于方法区之内,格式上符合描述一个Java类型信息的要求。经过这个阶段的验证之后,字节流才会进入内存的方法区进行存储

+ 2.元数据验证:对字节码描述的信息进行语义分析,说白了就是语法检查。比如implements 某个接口是否把所有函数都实现了,是否继承了final类

+ 3.字节码验证:最复杂的验证过程,进行数据流和控制流分析。保证被校验类的方法在运行时不会危害虚拟机。比如跳转指令不会跳转到方法体外的字节码指令上;类型转换是正确的;

+ 4.符号引用验证:将符号引用转化为直接引用,验证符号引用中通过字符串全限定名能否找到对应的类。

3.准备

准备阶段是在方法区为类的静态属性分配内存,并设置初始值。 这时候进行内存分配的只有类变量而不包括类的实例变量,因为实例变量将在对象实例化的时候随着对象一起分配到Java堆上。仔细想想道理很简单啊,因为方法的东西是该类所有对象共享的,所以只保存一份即可。而对象的实例肯定是各自持有,所以在堆上分配对象的时候再把实例变量分配一下。

比如

public static int value = 123;

public static final int value = 123;

那么value在准备阶段的初始值为0,而不是123。因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法中,所以把value赋值123的动作将在初始化阶段才会执行。上面说的通常情况,也有一些特殊情况:如果某些static字段是final修饰的,那么value会在编译时被javac生成ConstantValue属性,那么在准备阶段就会初始化为123。(所以static final不是编译期被赋值的,而是仅仅标记为ConstantValue属性,准备阶段才初始化对应的值)

4.解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 1.符号引用:以一组符号来描述所引用的目标,符号引用可以是任意值,只要能定位目标。符号引用与虚拟机的内存布局无关,而且目标不一定加载到内存中

  • 2.直接引用:直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是与虚拟机的内存布局有关的。所以同一个符号引用在不同的虚拟机中直接引用一般是不同的(因为可以有各自的内存布局)。如果有了直接引用,那么目标一定在内存中

5.初始化

准备阶段会对方法区内的属性进行一次“初始化”,而对于final修饰的则是在编译的时候加入ConstatnValue属性。而初始化阶段是根据程序员为程序制定的主观计划去初始化类变量和其他资源,或者可以说:执行类构造器()方法的过程。

  • ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,而顺序是按照在类中定义的顺序

  • ()方法与类的构造函数不同,它不需要显式调用父类构造器,虚拟机会保证在子类的()执行前,父类的()会执行完。因此,虚拟机第一个被运行的()一定是java.lang.Object的

  • 由于父类的()方法先执行,就意味着父类的静态语句块要优于子类的变量赋值操作,下面有个例子哦

  • 接口不一定有()方法,因为按接口的由来原理(通用协议),接口是没有属性的,在枚举以前可以用接口来完成常量的定义,但是有了枚举,还是使用枚举比较好

  • 若接口有变量初始化的赋值操作,这时候的()不需要先执行父类的()方法。只有用到父接口定义的变量时才会触发父类的()方法。同理,接口的实现类在执行()方法前也不需要执行接口的()方法

  • 虚拟机会保证一个类的()方法在多线程环境中被正确的加锁和同步。如果多个线程同时初始化一个类,那么只会有一个线程执行类的()方法,其他线程会被阻塞等待,直到()完成。

类加载器

1.背景

类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。很奇葩的是现在 Java Applet 被淘汰了,但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为了Java的一大王牌,可谓失之东隅,收之桑榆。

因为Java的广告就是一次编写到处运行,所以Sun将Java语言和JVM当成两个产品来开发。而JVM对应的《Java虚拟机规范》就是为了实现多输入,统一处理的目的

2.基本概念
对于Java中的任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在JVM中的唯一性。所以,看两个类是否相等(Class对象的equals()方法等),前提就是由一个类加载器加载的。如果不是一个类加载器加载的,即使是同一个.class文件也肯定是不相等的。理解这点是开发自己的类加载器的大前提。
  • 类加载器是怎样工作的

类加载器就是用来把Java文件加载到jvm中运行的。下面是运行的大概过程:

+ 1.编写Java源代码程序

+ 2.Java编译器编译成.class文件

+ 3.类加载器将.class文件加载进jvm,转换成java.lang.Class的一个实例,每个这样的实例代表一个Java类。通过这个实例的newInstance()方法可以创建出该类的一个对象
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;
        }
    }

类加载的步骤可以分为:

  • 1.检查这个类是否已经被加载过

  • 2.如果没有被加载过,调用父类加载器去加载

  • 3.如果父类加载器加载失败,就调用当前类加载器去加载

3.双亲委派模型

Java 中的类加载器大致可以分成两类,一类是系统提供的引导类加载器,它是用C++语言实现的,是JVM自身的一部分;另外一类则是其他所有的类加载器,这些类加载器都是用Java语言实现的,独立于虚拟机的外部,并且全部继承自java.lang.ClassLoader:

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用C++实现的,负责将${JAVA_HOME}/lib目录下的,或者-Xbootclasspath参数所指定的路径中的,并且是Java虚拟机识别的(仅按照文件名识别,如rt.jar,不符合的类库即使放在lib下也不会被加载)类库加载到JVM内存中,引导类加载器无法被Java程序直接引用

  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库,${JAVA_HOME}/ext下面的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可用

  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

    工作过程:
    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,那么它首先会把这个请求委派给父加载器完成,以此类推。因此所有的类加载请求最终都应该传送到顶层的引导类加载器中,只有当父加载器无法完成这个加载请求,子加载器才会尝试自己去加载。那么,回到上面的问题。为什么要使用这种代理机制呢?

你可能感兴趣的