文章目录
- 总体图
- 类装载子系统
- 一、类的加载过程
- 一、加载
- 二、链接
- 三、初始化
- 二、类的加载器
- 运行时数据区
- 一、程序计数器(ProgramCounter)
- 二、虚拟机栈( Java Stack )
- 三、本地方法栈( Native Method Stack )
- 四、堆内存(Direct Memory)
- 五、方法区(Method Area)
- 执行引擎区域
- 一、Java执行引擎
- 二、本地方法接口JNI
总体图
这个架构可以分成三层看:
- 最上层:javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域
- 中间层:称为Runtime Data Area,主要是在Java代码运行时用于存放数据的,从左至右为方法区(永久代、元数据区)、堆(共享,GC回收对象区域)、栈、程序计数器、寄存器、本地方法栈(私有)
- 最下层:解释器、JIT(just in time)编译器和 GC(Garbage Collection,垃圾回收器)
类装载子系统
一、类的加载过程
一、加载
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
二、链接
验证(Verify):
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备(Prepare):
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
解析(Resolve):
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。
- 在解析阶段,jvm根据字符串的内容找到内存区域中相应的地址,然后把符号引用替换成直接指向目标的指针、句柄、偏移量等,这些直接指向目标的指针、句柄、偏移量就被成为直接引用。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
三、初始化
- 初始化阶段就是执行类构造器方法()的过程。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- ()不同于类的构造器。(关联:构造器是虚拟机视角下的())
- 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
- 虚拟机必须保证一个类的()方法在多线程下被同步加锁。
二、类的加载器
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
双亲委派机制
ClassLoader类使用了委托模型来寻找类和资源,ClassLoader的每一个实例都会有一个与之关联的父ClassLoader,当ClassLoader被要求寻找一个类或者资源的时候,ClassLoader实例在自身尝试寻找类或者资源之前会委托它的父类加载器去完成。虚拟机内建的类加载器,称之为启动类加载器,是没有父加载器的,但是可以作为一个类加载器的父类加载器
破坏双亲委派机制
- JDK1.2前的代码无法再以技术手段避免loadClass()被子类覆盖的可能性
- JNDI(资源查找和管理)在应用程序的ClassPath下的JNDI服务提供者SPI接口,由父类加载器去请求子类加载器完成类加载的行为
- 代码热替换(Hot Swap)、模块热部署(Hot Deployment)
自定义类加载器
- 隔离加载类,避免类冲突
- 修改类加载的方式,根据实际情况在某个时间点按需动态加载
- 扩展加载源:网络、数据库、机顶盒
- 防止源码泄漏
沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样
运行时数据区
一、程序计数器(ProgramCounter)
- 具有线程隔离性
- 占用的内存空间非常小,可以忽略不计
- java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域
- 程序执行的时候,程序计数器是有值的,其记录的是程序正在执行的字节码的地址
二、虚拟机栈( Java Stack )
一个线程对应一个栈,一个栈对应多个方法栈帧, 栈帧包含局部变量表、操作数栈、动态连接、方法出口等
int i = 8;
i = i++;0 bipush 8 压栈2 istore_1 赋值出栈3 iload_1 压栈4 iinc 1 by 1 +17 istore_1 赋值出栈8 getstatic #3 <java/lang/System.out>
11 iload_1
12 invokevirtual #4 <java/io/PrintStream.println>
15 return
-
基于寄存器的指令集
-
基于栈的指令集
- HotSpot中的Local Variable table = JVM中的寄存器
-
JVM指令主要分为:本地变量表到操作数栈类指令、操作数栈到本地变量表类指令、常数到操作数栈类指令、将数组指定索引的数组推送至操作数栈类指令、将操作数栈数存储到数组指定索引类指令、操作数栈其他相关类指令、运算相关类指令、条件转移类指令、类和数组类指令和其他指令。
-
i开头的指令操作数类型是integer类型,l开头的指令操作数类型是long类型,f开头的指令操作数类型是float类型,d开头的指令操作数类型是double,a开头的指令操作数类型是引用类型(reference)。
-
load类指令将数据从本地变量表加载到操作数栈,store类指令将数据从操作数栈存储到本地变量表中。其他的指令主要用于操作数栈。
三、本地方法栈( Native Method Stack )
- Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者是可动态扩展的内存大小
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
- 本地方法一般是使用C语言实现的。
- 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
注意点:
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 它甚至可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一
四、堆内存(Direct Memory)
- 年轻代:新对象和没达到一定年龄的对象都在年轻代。
- 老年代:被长时间使用的对象,内存空间应该要比年轻代更大。
- 元空间:元空间不在虚拟机设置的内存中,而是使用本地内存。是对方法区的一种实现
- 直接内存: Java堆外的、直接向系统申请的内存区间
- StringTable
- Java 6及以前,字符串常量池存放在永久代。
- Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。
- Java 8 中,字符串常量仍然在堆
五、方法区(Method Area)
-
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
-
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、 JSP的重加载等,否则通常是很难达成的。
- 该类对应的java. lang. Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
-
方法区在JVM启动的时候被创建,并且它实际的物理内存空间中和Java堆区一样都可以是不连续的。
-
方法区的小大,跟堆空间一样,可以选择固定大小或者可扩展
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMenoryError:Metaspace
-
关闭JVM就会释放这个区域的内存
变化
JDK1.6及其以前:有永久代,静态变量存放在永久代上。
JDK1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中。
JDK1.8及其之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆。
执行引擎区域
一、Java执行引擎
执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者
- 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
- 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
- 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
JIT (Just In Time Compiler)编译器(即时编译器):是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行
HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。
- Interpreter模块:实现了解释器的核心功能
- Code模块:用于管理HotSpot VM在运行时生成的本地机器指令
热点代码及探测方式
-
热点代码:一行代码或者一段逻辑被多次频繁调用,JIT会将其编译成机器码指令,提高代码运行效率
-
热点探测功能 :JIT编译器通过一个阈值才会将这些“热点代码”编译为本地机器指令执行
-
采用基于计数器的热点探测
-
方法调用计数器用于统计方法的调用次数
-
回边计数器则用于统计循环体执行的循环次数
HotSpot VM 可以设置程序执行方式
- -Xint: 完全采用解释器模式执行程序;
- -Xcomp: 完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
- -Xmixed:采用解释器+即时编译器的混合模式共同执行程序
HotSpot VM 中的JIT分类
- client: 指定Java虚拟机运行在Client模式下,并使用C1编译器;
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。 - server: 指定Java虚拟机运行在Server模式下,并使用C2编译器。
C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高 - Graal编译器: 使用了 Graal 编译器技术,采用 Java 写的 JIT 编译器, 让 Java 可以在一个运行期内,同时使用多种语言
- AOT编译器:jdk9引入的静态提前编译器, 在程序运行之前,便将字节码转换为机器码的过程
二、本地方法接口JNI
JNI:Java Native Interface 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序
- Java诞生的时候C、C++横行,为了立足,必须要能调用C、C++的程序
- 于是在内存区域中专门开辟了一块标记区域:Native Method Stack,登记Native方法
- 最终在执行引擎执行的的时候通过JNI(本地方法接口)加载本地方法库的方法