JVM——类加载
一、什么是类加载?
JVM将class字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
二、类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、链接(验证、准备、解析)、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:
2.1 加载
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类
加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。
注意:第一条中的数据来源不单单指从class文件中获取,通常有如下几种来源:
- 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
- 从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
- 通过网络加载class文件。
- 把一个Java源文件动态编译,并执行加载
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在 Java 堆中也创建一个 java.lang.Class 类的对象,这样便可以通过该对象访问方法区中的这些数据。
2.2 验证
为了确保 Class 文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证
文件格式验证:主要验证字节流是否符合Class文件格式规范(ca fe ba be),并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
2.3 准备
准备阶段是正式为类变量分配内存并设置类变量默认值的阶段(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间
注意:
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
2.4 解析
将常量池中的符号引用转化为直接引用的过程。
2.4.1 符号引用和直接引用:
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中,布局与内存无关。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
2.4.2 四种解析:
1.类或接口的解析 2.字段解析类 3.方法解析 4.接口方法解析
具体理解请见 http://wiki.jikexueyuan.com/project/java-vm/class-loading-mechanism.html
2.5 初始化
初始化可以说是类加载的最后一个阶段,到了此阶段,才真正开始执行类中定义的 Java 程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器<cinit>()V 方法的过程,虚拟机会保证这个类的『构造方法』的线程安全
发生时机
概括得说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
三、类加载器
类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就 Java 虚拟机中的唯一性,也就是说,即使两个类来源于同一个 Class 文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的 Class 对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用 instanceof 关键字对对象所属关系的判定结果。
类加载器可分为以下四类:
3.1 启动类加载器(Bootstrap ClassLoader)
它使用 C++ 实现,负责加载存放在JDK\jre\lib
(JDK 代表 JDK 的安装目录,下同)下,或被-Xbootclasspath
参数指定的路径中的,并且能被虚拟机识别的类库(如 rt.jar,所有的java.*
开头的类均被 Bootstrap ClassLoader 加载)。启动类加载器是无法被 Java 程序直接引用的。
3.2 拓展类加载器(Extension ClassLoader)
该加载器由sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext
目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如javax.*
开头的类),开发者可以直接使用扩展类加载器。
3.3 应用程序类加载器:(Application ClassLoader)
该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
3.4 用户自定义加载器 :(Customized Class Loader)
用户可以自己定义类加载器来加载类。所有的类加载器都要继承 java.lang.ClassLoader
类并重写 findClass(String name)
方法。用户自定义类加载器默认父加载器是 应用程序加载器
四、类加载机制
4.1 全盘负责委托机制
当进行类加载的时候,如果手动指定了ClassLoader,那么该类所依赖和引用的类也由这个类加载器进行加载
User->UserParent
指定User使用特定的类加载器,那么跟User类有依赖和引用关系的类也用这个类加载器进行加载
4.2 双亲委派机制
4.2.1 工作原理:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
4.2.2 优势:
Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。例如,类java.lang.Object 类存放在JDK\jre\lib
下的 rt.jar 之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了 Object 类在程序中的各种类加载器中都是同一个类。
4.2.3 图解:
4.2.4 源码:
//提供class类的二进制名称表示,加载对应class,加载成功,则返回表示该类对应的Class<T> instance 实例 public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查是否已经被当前的类加载器记载过了,如果已经被加载,直接返回对应的Class<T>实例 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; } }
五、破坏双亲委派机制
5.1 为什么要破坏双亲委派机制?
在某些情况下父类加载器需要委托子类加载器去加载class文件
5.2 怎么样破坏双亲委派机制?
1.重写ClassLoad类中的loadClass方法
2.手动调用系统类加载器 Thread.currentThread().getContextClassLoader();
3.OSGi
具体了解:https://www.cnblogs.com/fengtingxin/p/11872710.html 和 https://www.cnblogs.com/joemsu/p/9310226.html
六、总结
1. 根据JVM内存配置要求,为JVM申请特定大小的内存空间;
2. 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;
3. 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader;
4. 使用上述获取的ClassLoader实例加载我们定义的 org.luanlouis.jvm.load.Main类;
5. 加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;
6. 结束,java程序运行结束,JVM销毁。
若要了解Launcher等每一步流程,可参考https://blog.csdn.net/luanlouis/article/details/50529868