JVM内存结构JMM
JVM 内存模型
从JVM的官方文档上可以看见,JVM的内存结构主要包含三块: 堆内存、方法区、栈
堆中又分 新生代 和 老年代, 新生代包含 Eden空间、From Survivor空间、To Survivor空间,也是线程共享的区域
方法区(非堆区)存储的 类信息、常量、静态变量等数据,是线程共享的区域,
栈 分为 本地方法栈 和 JVM虚拟机栈,还有个程序计数器 这三个都是线程私有的
- Java Heap
Java Heap 是 JVM 虚拟机管理的内存最大的一块,Java Heap 被所有的线程共享的内存区域,几乎所有的对象都在这里分配,也是 GC 管理的主要区域 Java Heap 是在物理上不连续的内存空间,只要逻辑上连续即可。
- Method Area
Method Area(非堆) 和 Java Heap 一样,是🧍各个线程共享的内存区域,用于存储已经被虚拟机加载的 类信息、常量、静态变量、即使编译器编译的代码等等
- Program Counter Register
Program Counter Register 程序计数器,较小的内存空间,是当前线程锁执行的字节码的行号指示器(唯一一个没有OOM的区域)
- JVM Stacks
JVM Stacks 也是线程私有的,它的生命周期和线程相同,虚拟机栈描述的 Java 方法执行的内存模型: 创建栈帧 Stack frame 用于存储 局部变量表、操作栈、动态链接、方法出口 等信息 每次方法被调用直至完成,都对应着一个栈帧在虚拟机栈中的入栈和出栈过程。
JVM 两种异常:
线程请求栈深度大于虚拟机允许的深度 StackOverflowError
JVM栈一般动态扩展的,如果无法申请足够的内存,会抛 OutOfMemoryError
Native Method Stacks
Native Method Stacks 与 JVN Stacks 功能非常相似,本地方法栈是为虚拟机使用到 Native方法服务。
GC垃圾回收器 GC算法
判断对象是否存活:
- 引用计数
- 可达性分析
Java堆内存被分代管理,分代也是为了快速方便的垃圾回收,针对不同的代,使用不同的回收算法方式
垃圾分代回收算法
- 标记清除算法
- 复制算法
- 标记压缩算法
垃圾分代回收算法
- 标记清除算法 Mark-Sweep,分为
标记``清除
两个阶段, 首先标记出所有需要回收的对象、后面统一回收掉被标记的对象。
特点
- 效率问题,标记阶段、清除阶段的效率都不高
- 空间问题,标记清除之后产生大量的内存碎片,空间碎片太多
- 复制算法 Copying,复制算法将内存容量划分成大小相等的凉快,每次只使用其中一块。当一块使用完毕,将存活的对象迁移到另外一块,然后将已经使用的全部清理掉。这种算法适合新生代
特点
- 解决了内存碎片问题,但是代价是内存缩小到原来的一半
- 持续复制长时间生成的对象,效率低
标记压缩算法 Mark-Compact,分为
标记
整理
阶段,首先标记过程也是标记出需要清除的对象,后续是让所有的存活的对象向一端一端,然后清理边界以外的内存。分代收集算法,为什么会有分代收集,Java堆分为新生代和老年代,且根据各个年代的特点选择合适的收集算法。新生代:绝大部分对象会很快死亡,只有少量存在,就选择复制算法。 老年代: 大部分对象都是存活很高,就可以选择 标记清理 或 标记整理 算法进行回收。
垃圾回收器
- Serial 收集器, 一个线程去回收,会产生 STW。新生代、老年代都是串行, 新生代复制,老年代标记压缩
- ParNew 收集器, 是 Serial收集器的多线程版本。新生代并行, 老年代串行, 新生代复制,老年代标记压缩
- Parallel 收集器, 类似 ParNew收集器,更关注系统的吞吐量,可以通过参数打开自适应调节策略。新生代并行,老年代串行, 新生代复制,老年代标记压缩
- Parallel Old 收集器,是 Parallel Scavenge 的老年代版本,使用多线程和标记整理算法, 新生代Parallel收集器,老年代并行, 新生代复制,老年代标记整理
- CMS 收集器(Concurrent Mark Sweep), 以获取最短的回收停顿时间为目标的收集器,基于 标记清除 算法实现的
总共4个步骤
- CMS initial mark 初始标记: 标记 GC Roots 能直接关联到的对象
- CMS concurrent mark 并发标记: 进行 GC Roots Tracing 过程
- CMS remark 重新标记: 修正并发标记期间, 因为程序继续运行而导致的标记产生变动的那一部分对象的标记记录, 这个阶段停顿时间比初始标记阶段稍长一些, 但远比并发标记时间短
- CMS concurrent sweep 并发清除
初始标记、重新标记,仍然需要 “Stop The World”,优点:并发收集,低停顿,缺点:大量空间碎片、并发阶段降低吞吐量
JMM (Java内存模型)
当今的计算机都是多处理器系统, 处理器通常是有多级缓存的,因为这些缓存李处理器更近可以存储一部分数据,所以缓存可以改善处理器获取数据的速度和减少对共享内存数据总线的占用。
Java 的并发采用的是共享内存模型, 所以 Java 线程之间的通信是由 Java 内存模型控制的,JMM 决定了一个线程对共享变量的写入何时对另一个线程可见。 JMM 定义了线程和主内存之间的关系: 线程间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程以 读/写 共享变量的副本(本地内存是JMM抽象概念,真实不存在)
JMM 三个特征:
- 原子性 : 一个操作不能被打断,要么全部执行完毕,要么不执行
基本数据类型访问大部分都是原子操作, 对于不是原子的,在多线程并发情况下是线程非安全的。
- 可见性 : 一个线程对共享变量做了变化,其他线程立即能够感知到变换
通过将工作内存中的变量修改后的值同步到主内存, 在读取变量前从主内存刷新最新的值到工作内存中,依赖主内存的方式实现可见行
要保证可见性 volatile 、synchronized 、Lock、final 等等也可以
- volatile 的特殊规则保证了 volatile 变量值修改后新值立刻同步到主内存, 每次使用 volatile 变量前都立即从主内存刷新, 所以 volatile 保证了多线程操作变量的可见性
- synchronized 同步方法/同步块 开始时(Monitor Enter) 使用共享变量时候会从主内存刷新变量到功罪内存中,在 同步方法/同步快 结束时(Monitor Exit) 会将工作内存变量值同步到主内存中
- Lock 使用 ReentrantLock(可重入锁)实现可见行, 在方法执行 lock.lock() 的是时候m 共享变量会从主内存刷新变量到工作内存中, 在 finally 代码块中执行 lock.unlock(), 将工作内存中变量值同步到主内存
- final 关键字可见性指的是 final 修饰的变量,在构造函数一旦完成
- 有序性
JVM G1 的内存模型
G1 GC (Garbage First Garbage Collector) 垃圾优先垃圾回收器,它是短停顿的垃圾回收器,为了取代 CMS 。
G1 的内存模型,相对之前的模型,已经不进行物理分代了,采用逻辑分代,不再对连续内存进行区分 新生代(Eden,From Survivor、To Survivor) 和 老年代(Old区)
从图中可以看到, G1 将内存分成一个个的 Region, 一块Region(分区)在逻辑上依然分代, 分为四种: Eden(伊甸区), Old(老年代), Survivor(幸存区), Humongous(大对象,横跨多个连续的Region)。
内存回收以 Region 为基本单位, Region回收使用的是 复制算法, 所以不存在内存碎片化问题,整体上属于标记压缩算法, 通过复制算法处理后内存会被整齐(region通过回收后被规整)
G1 是一种带压缩的垃圾收集器,在回收老年代的分区的时候, 将存活的对象从一个分区拷贝到另一个可用的分区, 这个拷贝的过程实现了局部压缩. 每个分区的大小从 1M~32M 不等, 都是 2 的幂次方
特点:
- 并发收集
- 压缩空间时间不会延长GC的暂停时间
- 更易观测GC的暂停时间
- 适合不需要实现很高吞吐量的场景
G1 回收过程
G1 回收过程主要有三个步骤
- 年轻代回收 yong gc
- 老年代并发标记 concurrent marking
- 混合回收过程 mixed gc
当年轻代的 Eden区 用尽时候,开始年轻代回收; 当堆内存使用达到一定的值(默认45%)时, 开始老年代并发标记; 标记完毕立刻执行混合回收过程。
G1: remember set 为了避免全局扫描, 解决跨代引用问题避免全局扫描
- 每个 region 都有自身对应的一个记忆集 RSet
- 每次引用类型数据写操作的时候,都会产生一个写屏障(Write Barrier)暂时的中断操作
- 然后检查将要写入的 引用指向的对象是否和该引用类型数据在不同的 region (其他收集器将会检查老年代对象是否引用新生代对象)
- 如果不同, 通过 CardTable 把相关引用信息记录到引用指向对象所在的 region 对象的 RSet 中
- 当进行垃圾回收时候, 在 GC Root 的枚举范围内加入 RSet,就可以保证不进行全局的对象的对象关系扫描
dirty card queue:
对于应用程序的引用赋值,如 obj1.field = obj2, JVM 会在之前和之后指向特殊的操作,在 dirty card queue 中入队以恶搞保存了对象引用信息的 card, 年轻代回收的时候, G1 会对 dirty card queue 中所有的 card 进行处理,用来更新 RSet,确保 RSet 实时准确的反应引用关系
年轻代GC
- 并行的、独占式的 (STW) 来及回收, 会发生对象的代晋升, 将会把对象放入 Survivor 或者老年代
- 当Eden区内存空间耗尽时候, 会启动一次年轻代垃圾回收,同时回收 Survivor 区
- G1 Young GC 的时候,先停止程序线程(STW), G1 创建 回收集 (Collection Set, 需要被回收的内存分段集合,包含了 Eden区 和 Survivor区 所有的内存分段)
- 第一阶段, 扫描 GC Roots, GC Roots 联通 RSet记录的外部引用作为扫描存活对象的入口
- 第二阶段, 处理 dirty card queue 中的 card, 更新 RSet,完成后 RSet 可以准确反应老年代对所在的内存分段中对象的引用
- 第三阶段, 处理 RSet, 识别被老年代对象指向的 Eden 区中的对象, 这些被指向 Eden 中的对象认为是存活的对象
- 第四阶段, 复制 对象 (复制算法), 对象树被遍历, Eden 区 region 中存活的对象会被复制到 Survivor 区中的 Region, Survivor Region 中存活的对象如果年龄未达到阈值, 年龄+1, 达到阈值的会被复制到 Old Region, 如果 Survivor 的空间不够用, Eden中部分数据会直接晋升到老年代
- 第五阶段, 处理 引用, 处理 Soft、Weak、Phantom、Final、JNI Weak 等应用,最终 Eden 区的数据为空, 这些空的 region 将会等待对象分配, GC 停止工作,目标内存中的对象也是连续的,没有内存碎片
老年代并发标记
当堆空间的内存使用达到阈值(默认 45%)开始老年代并发标记过程。
- 初始标记阶段:标记 GC Roots 直接可达对象,也就是直接引用关系对象,会发生 STW,但是暂停时间很短,触发一次 Young GC
- 根区域的扫描 (Root Region Scanning): G1 扫描 Survivor区直接可达的老年代区域对象,并标记为被引用的对象(在YoungGC之前完成,确保 YoungGC操作 Survivor区对象之后)
- 并发标记( Concurrent Marking): 在整个堆中进行并发标记,此过程可能会被 Young GC 打断,在并发标记阶段,若发现某些 region 的所有的对象都是垃圾,那么这个 region 就会被立即回收, 同时在标记过程中,会计算每个 region 的对象活性(该 region 存活对象的比例, G1 垃圾回收的时候并不是所有的 region 都会参与回收的, 根据回收的价值高低来优先回收价值较高的 region )
- 再次标记: 为了修正并发标记过程,程序线程继续并发指向产生的新的操作,这是修正上一次标记的结果, 增量补偿标记, 会出现 STW, G1采用的是 比CMS更快的初始快照算法 snapsho-at-the-beginning SATB
- 独占清理: 计算各个 region 的存活对象 和 GC 回收比例, 并进行排序(回收价值的高低排序), 识别可以混合回收的区域
- 并发清理阶段: 识别并且完成空闲的区域
混合回收
- 回收包含了年轻代和老年代
- 标记完成后马上开始垃圾回收, G1 从老年代移动存活的对象到空闲区域,这些空闲区域变成老年代的 region。当越来越多的对象晋升到老年代的 region 时候,为了避免内存耗尽,会触发混合垃圾收集 Mixed GC, 针对的是回收整个 Young region 和 一部分 Old region
- 并发标记结束之后, 老年代中能够完全确认为垃圾的 region 中的内存分段被回收,部分为垃圾的 region 中的内存分段也被计算出来。默认情况下,这些老年代的内存分段会被分8次回收
- 混合回收的回收集包括 1/8 的老年代的内存分段,、Eden 区内存分段、Survivor区内存分段
- 由于老年代的内存分段默认分 8 次回收(当然可以通过参数更改), G1 会优先回收垃圾较多的内存分段, 垃圾占用内存分段比例高的会优先被回收
必要情况下(对象分配速度大于回收速度) Full GC 触发
垃圾收集器的特点
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 适合单 CPU 环境 client 模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 适合多 CPU 环境 Server 模式 与 CMS 配合使用 |
Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行 | 老年代 | 标记压缩算法 | 响应速度优先 | 适合单 CPU 环境 client 模式 |
Parallel Old | 并行 | 老年代 | 标记压缩算法 | 吞吐量优先 | 适用于后台运输而不需要太多交互的场景 |
CMS | 并发 | 老年代 | 标记清除算法 | 响应速度优先 | 适合互联网或 B/S业务 |
G1 | 并发、并行 | 新生代、老年代 | 标记压缩算法、复制算法 | 响应速度优先 | 面向服务器端应用 |