本来计划将JVM的所有知识点做一个总结,发现在一篇里的篇幅太长了,故做了切分,后面会按照jvm结构图一步一步的讲解下去。
1.什么是JVM?JVM的基础结构是啥样的?
1.1 JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了实现跨平台。下图解释了JDK、JRE、JVM的关系:
1.2JVM的基础结构
宏观上而言,JVM被分为三个主要的子系统:类加载器子系统、运行时数据区、执行引擎,结构图如下所示:
1.2.1类加载器子系统
Java的动态类加载功能由类加载器子系统处理,处理过程包括加载和链接,并在类文件运行时,首次引用类时就开始实例化类文件,而不是在编译时进行。
1.2.1.1loading加载详解
ClassLoader 做什么的?顾名思义,它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
启动(Bootstrap)类加载器
引导类加载器是用 本地代码实现的类加载器,它负责将 <JAVA_HOME>/lib下面的核心类库 或 -Xbootclasspath选项指定的jar包等 虚拟机识别的类库 加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以 不允许直接通过引用进行操作。
扩展(Extension)类加载器
扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System、Application)类加载器
系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库 加载到内存中。开发者可以直接使用系统类加载器。
这一块内容也是面试最常见的问题,可以引申出什么是双亲委派模式?双亲委派模式优势?
什么是双亲委派模式?
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派模式优势?
(1).采用双亲委派模式的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
(2).其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
1.2.1.2linking链接详解
链接的过程分了三步,分别是验证、准备、链接。过程如下所示:
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。文件格式验证、元数据验证、字节码验证、符号引用验证。各阶段的具体验证都有详细的说明,这里做面试篇讲解,就不针每个验证做详细说明了,需要的话可以自行查询学习一下,如果需要,可以找我要一下资料。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值(通常情况下是数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。这个过程包含了类或接口的解析、字段解析、类方法解析、接口方法解析。
这一块内容也是面试最常见的问题,可以引申出什么是符号引用?什么是直接引用?准备阶段给变量如何赋值?等等。
1.2.1.3初始化
初始化 初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源。分为主动引用和类的被动引用。
主动引用
有且只有下面5种情况才会立即初始化类,称为主动引用:
1、new 对象时;
2、读取或设置类的静态字段(除了被final,已在编译期把结果放入常量池的静态字段)或调用类的静态方法时;
3、用java.lang.reflect包的方法对类进行反射调用没初始化过的类时Class.forname()会进行初始化
4、初始化一个类时发现其父类没初始化,则要先初始化其父类;
5、含main方法的那个类,jvm启动时,需要指定一个执行主类,jvm先初始化这个类。
类的被动引用(不会发生类的初始化)
1、当访问一个静态变量时,只有真正实现这个静态变量的类才会被初始化(通过子类引用父类的静态变量,不会导致子类初始化)
2、通过数组定义类应用,不会触发此类的初始化 A[] a = new A[10];
3、引用常量(final类型)不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
实例化的顺序
这一个也是重点,经常出现在面试题的选择题里,了解一下。
好了,到这里JVM类加载过程的知识点就算完结了,下一篇将会开始说运行时数据区的相关知识点和面试,这个也是重点,大部分的面试题都是从这里衍生出来的。
本文是学习了王军伟的JVM课程后,对其做的总结和整理,大家也可以去听听他的课,讲的不错的。