Java虚拟机
Contents
JVM组成
- 类加载器
- 运行时数据区
- 执行引擎
- 本地接口库
类加载器
JVM把描述类的数据从二进制字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被称为虚拟机的类加载机制。
分类
- 启动类加载器
- 扩展类加载器
- 应用程序类加载器
- 自定义类加载器
双亲委派模型
- 一个类加载器接收到类加载任务,会先交给父类加载器去完成,只有确认父类加载器无法完成加载任务时,才会尝试进行类加载
- 类加载器和全限定类名才能确定其在JVM中的唯一性。
- 打破双亲委派模型
- 重写 loadClass() 方法
- 使用线程上下文类加载器
类加载过程
- 加载
- 通过一个类的全限定类名来获取其定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为
方法区
的运行时数据结构 - 在
堆
中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口 - 类变量(静态变量)随着 Class 对象一起存放到堆中。
- 连接
- 验证
- 确保 Class 文件的字节流中包含的信息符合JVM规范的全部约束要求,确保这些信息被当作代码运行以后不会危害JVM自身。
- 准备
- 正式为类中定义的变量(静态变量)分配内存并设置类变量初始值的阶段(类变量、实例变量、局部变量)
- 实例对象会在对象实例化时随着对象一起分配到堆中。
- 解析
- JVM将常量池内的符号引用替换为直接引用的过程(符号引用:在二进制字节码内定位常量;直接引用:在JVM虚拟机内定位常量)。
- 验证
- 初始化
- 执行类构造器方法的过程。
运行时数据区
JVM内存模型,一共五个部分;线程私有:程序计数器、java虚拟机栈、本地方法栈;线程共享:堆和方法区
- 程序计数器
- 当前线程所执行的字节码的行号指示器,控制程序执行流程:分支,循环,跳转,异常处理,线程恢复。
- 每条线程都有一个独立的程序计数器,是“线程的私有”内存。
- 若执行的是本地(Native)方法,这个计数器的值应该为空
- Java虚拟机栈
- 描述的是“Java方法执行的线程内存模型”
- 也是线程私有的!
- 每个方法执行的时候,Java虚拟机都会同步创建一个栈帧用于存储:局部变量表(存放了编译期可知的各种Java虚拟机基本数据类型,对象引用,returnAddress)、操作数栈、动态链接、方法出口等信息。
- 本地方法栈
- 为虚拟机使用到的本地(Native)方法服务。
- Java堆
- 被所有线程共享
- 从分配内存的角度看,所有线程共享的Java堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer)(TLAB)
- 只存放对象实例。
- 逻辑上连续
- 字符串常量池
- 方法区
- 线程共享
- 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- JDK6之前,永久代是方法区的一种实现,但JDK6之后已经开始逐渐放弃永久代,转用本地内存来实现方法区了。
- JDK8完全放弃永久代的概念,改用在本地内存中实现的元空间(Mate-space)来代替
- 永久代容易导致内存溢出问题
- 运行时常量池、静态常量池保存在元空间中
- 字符串常量池保存在堆中!
- 字面量:java语言层面的常量概念
- 符号引用: 类和接口的全限定类名;字段名称和描述符;方法名和描述符;
垃圾回收
回收算法
- 标记-清除
- 标记-整理
- 标记-复制
垃圾判定
- 引用计数法
- 可达性分析法(执行上下文、全局性引用)
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 被方法区中静态变量、常量引用的对象
- 被synchronized持有的对象
分代收集理论
- 分代假说
- 绝大多数对象都是朝生夕灭的。
- 熬过越多次垃圾收集过程的对象就越难以消亡
- 跨代引用相对于同代引用来说仅占小数(所以,无需为了少量跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在以及哪些跨代引用,只需要在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分为若干小块,标识出哪一块内存会存在跨代引用。此后发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC root进行扫描)
- 新生代:老年代 = 1:2
- 伊甸区:survivor:survivor = 8:1:1
HotSpot的算法实现细节
- 根节点枚举
- 无需遍历检查完所有执行上下文、全局引用位置,而是应该可以直接得到哪些地方存着对象引用的!
- 利用一组OopMap的数据结构达到以上目的!
- 对象类型内有记录自己的OopMap,记录了该类型的对象内什么偏移量上是什么类型的数据????
- 通过oopmap快速定位到对象位置?
- 安全点
- 为每条指令都生成OopMap带来的成本是无法接受的
- 只在特定位置生成OopMap,这些位置称为安全点!
- 安全区域
- 没有被执行的代码的安全点解决方案
- 记忆集与卡表(用于跨代引用)
- 记忆集是一种用于记录从
非收集区域
指向收集区域
的指针集合的抽象数据结构 - 垃圾收集的场景中,只需要通过记忆集判断某一块区域是否存有指向收集区域的指针就可以了,根据
某一块区域
的粒度大小,可分为字长精度、对象精度、卡精度。 - 卡精度是借助卡表来实现记忆集
- 卡表和记忆集的关系可以理解为HashMap和Map的关系(卡表是记忆集的具体实现?)。
- 卡表就是一个字节数组,其中每个元素代表内存区域中一块特定大小的内存块(卡页)。
- 一个卡页内存中通常不止包含一个对象,但只要有一个对象存在跨代引用,那么对应的卡页就标识为1(变脏)
- 记忆集是一种用于记录从
- 写屏障(卡表的具体实现)
- 有其他分代区域中对象引用了本区域对象时,对应卡页元素就应该变脏
- 必须找到一个机器码层面的手段把维护卡表的动作放到每一个赋值操作之中
写屏障
不同于防止指令重排的内存屏障
,而是相当于“引用类型字段赋值”这个动作的AOP切面!
垃圾回收器(并发和并行)
收集器 | 串行/并行/并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的client模式 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面型服务端应用 |
- ZGC:
- 在jdk14中移除了CMS,
- jdk13发行ZGC
- jdk14 ZGC支持所有平台
- ZGC回收过程
- 标记
- 迁移
- 重映射
- CMS(Concurrent Mark Sweep)
- 初始标记:STW
- 并发标记
- 重新标记:STW
- 并发清除
- CMS缺点
- 无法处理浮动垃圾:在标记过程后出现的垃圾,容易导致Full GC
- 空间碎片!
- Garbage First(G1)
- 能够建立起
时间停顿模型
的收集器,在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间大概率不超过N毫秒。 - 也就是说
垃圾回收的时间必须可控
! - 垃圾收集的范围不再局限于新生代(Minor GC)或者老年代(Major GC)或者整个堆(Full GC),而是针对一个由内存块组成的回收集。
- 把Java堆分成多个相等独立的region,每个region都可以根据需要扮演Eden、Survivor、老年代等。
- 还有一类特殊的区域,专门用于存放大对象。
- 主要的难点在于不同region之间的相互引用
- 解决思路是利用之前用于解决跨代引用的
记忆集
来实现region内存活对象的判断 - G1中的每个region都有一个对应的
记忆集
,虚拟机发现程序在对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,并检查持有Reference的对象和Reference指向的对象是否处于同一个region,如果不是,就把相关信息记录到Reference指向对象所属的region的记忆集
中 - 内存分配算法是TLAB
- 能够建立起
- G1回收过程
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
Author 段新朋
LastMod 2020-07-13