内存布局图:
一、垃圾回收算法
(1) 标记-清除法
该算法会从每个 GC Roots
出发, 依次标记引用关系的对象, 最后将没有被标记的对象清除
此算法带来的后果:
- 带来大量的空间碎片
若需要分配一个较大连续空间时容易触发 FGC
就好比在操作系统中对碎片空间的整理
(2) 标记-整理法(Mark-Copy)
此类算法类似计算机的磁盘整理
步骤如下: (不考虑存活对象超过S0
或 S1
)
- 第一次
Minior GC
,Eden
区被清空,Eden
存活对象移至S0
- 第二次
Minior GC
,Eden
区被清空,Eden
存活对象和S0
存活对象移至S1
,S0
区清空 - 之后, 存活对象在
S0
与S1
中交换
Eden
、S0
、S1
内存容量分配
Eden
占 80%,S0
占 10%,S1
占 10%为什么这样区分?
为了更大利用内存容量.
二、垃圾回收器(Garbage Collector)
垃圾回收器(Garbage Collector)是实现垃圾回收算法并应用在
JVM
环境中的内存管理模块
(1) Serial
回收器
Serial
回收器是一个主要应用于 YGC 的垃圾回收器, 采用串行单线程的方式完成 GC 任务
“Stop The World” 简称 STW, 即垃圾回收的某个阶段会暂停整个应用程序的执行
现在写后台
Java
系统几乎不用
主要流程如图:
(2) ParNew
回收器
ParNew
回收器主要用于新生代, 采用多线程机制
理论上4核CPU, 支持4个垃圾回收线程并行执行
执行Minor GC
, 也采用 STW, 会把系统程序的工作线程全部停掉, 禁止程序继续运行创建新的对象, 采用多个垃圾回收线程去进行垃圾回收
-XX:+UseParNewGC
: 表示JVM
启动之后对新生代进行垃圾回收
-XX:ParallelGCThreads
: 调节ParNew
的垃圾回收线程数量
一般采用默认, 例如: 4核CPU 8核CPU 16核CPU, 对应
ParNew
线程数为 4、8、16
(3) CMS
回收器(Concurrent Mark Sweep Collector)
CMS
回收器 是回收停顿时间比较短、用于老年代的垃圾回收器, 采用多线程机制, 性能较好
四个步骤完成垃圾回收:
- 初始化标记(Initial Mark)
- 并发标记(Concurrent Mark)
- 重新标记(Remark)
- 并发清除(Concurrent Sweep)
1、3 步的初始化标记和重新标记阶段依然会引发
STW
,
而 2、4步的并发标记和并发清除两个阶段可以和应用程序并发执行, 也是比较耗时的操作, 但并不影响应用程序的正常执行
CMS
采用 “标记 - 清除算法”, 因此产生大量的空间碎片
-XX:+UseCMSCompactAtFullCollection
强制JVM
在FGC
完成后对老年代进行压缩, 执行一次空间碎片整理, 但是空间碎片整理阶段也会引发STW
-XX:+CMSFullGCsBeforeCompaction=n
: 在执行了 n 次FGC
后,JVM
再在老年代执行空间碎片整理, 减少 STW 次数.
(4) G1
回收器(Garbage-First Garbage Collector)
目的:为了减少Stop the World
Hotspot 在 JDK7 中推出了新一代 G1, 通过
-XX:+UseG1GC
开启
优点:
- 和 CMS 相比, G1 具备压缩功能, 能避免碎片问题
- G1 的暂停时间(
STW
)更加可控 - 统一收集新生代和老年代, 采用了更加优秀的算法和设计机制
- 可预测的停顿时间,能够尽可能地在指定时间内完成空间碎片,
-XX:MaxGCPauseMills
参数来设定,默认值为 200ms
如图:G1
回收模型内存布局
G1
将 Java
堆空间分割成了若干相同大小的区域,即 region
region
包括Eden
、Survivor
、Old
、Humongous
四种类型Humongous
是特殊的Old
类型,专门放置大型对象。- 这样划分意味着不需要一个连续的内存空间管理对象
G1
采用 Mark-Copy
G1
执行时使用 4个 worker并发执行,在初始标记时,还是会触发STW
1. G1
是如何工作的?
G1
需要知道每个Region
有多少垃圾,处理这些垃圾需要多长时间?
根据需要回收对象的大小和回收预估时间,进行选择回收Region
如图:
2. 对象什么时候进入新生代的Region
?什么时候对象进入老年代 Region
?
刚开始,
Region
可能谁都不属于。
- 当有对象产生,则分配给了新生代
- 触发
GC
- 下一次,同一个
Region
可能又被分配给了老年代,用来存放老年代的对象
如图:
新生代进入老年代的条件:
- 对象在新生代躲过了很多此的垃圾回收,达到了一定的年龄,可通过
-XX:MaxTenuringThreshold
设置年龄 - 动态年龄判定规则,如果一旦发现某次新生代 GC 过后,存货对象超过了
Survivor
的50%
3. 什么时候触发Region GC
?什么时候触发老年代的Region GC
?
G1
将内存划分为多个Region
,但还是有 新生代、老年代的区分
新生代里还是有Eden
和Survivor
划分
触发垃圾回收的机制也相类似
4. Humongous
大对象Region
G1
提供了专门的Region
来存放大对象,而不是让大对象进入老年代Old
的Region
中
大对象的判定规则:
一个大对象超过一个
Region
大小的 50%,就会被放入大对象专门的Region
中
比如:
每个Region
2MB,只要一个大对象超过 1MB,就会被放入大对象专门的Region
中
如图:
三、问题
(1) 单线程与多线程进行垃圾回收比较
单CPU运行多线程会导致频繁的线上切换上下文, 有效率开销
即:
- 若在客户端(客户端程序), 采用单线程垃圾回收器
- 若在服务端(有多核CPU资源), 采用多线程垃圾回收器
(2) ParNew
+ CMS
的 GC
, 如何保证只做 YGC
, JVM
参数如何配置?
- 加大分代年龄, 比如默认 15 加到 30
- 修改新生代和老年代的比例, 比如新生代:老年代 = 2:1
- 修改
Eden
区 和S0
S1
区比例, 例如 6: 2: 2
(3) 如何做到FullGC
次数为0, 只做 YGC
?
关键点: 让
Survivor
区能放下, 不能因为动态年龄判断规则直接升入老年代
观察上线系统, 每秒会新增多少对象在新生代里, 多长时间触发一次 Minior GC
, 平均每次 Minor GC
之后会有多少对象存活, Survivor
区是否可以放下
(4) 有哪些参数需要了解?
2核4G机器, 可提供 JVM 最大内存 2G
- 方法区(元空间)
- 老年代
- 新生代
- 栈
(5) 为什么老年代的 FGC
要比 新生代Minior GC
慢很多, 一般10倍以上?
- 并发标记阶段, 老年代存活对象多, 追踪
GC Roots
花费要久 - 并发清理阶段, 老年代不是一次性回收一大片内存, 而是零散
- 内存碎片整理, 把大量的存活对象给挪在一起, 空出来连续内存空间, 这个过程需要
STW
(6) 几个触发老年代 GC 的时机?
-
老年代可用内存小于新生代全部对象的大小, 如果没开启空间担保参数, 会直接触发
FGC
, 所有一般空间担保参数都会打开 -
老年代可用内存小于历次新生代
GC
后进入老年代的平均对象大小, 此时会提前FGC
-
新生代
Minior GC
后的存活对象大于Survivor
, 那么就会进入老年代, 此时老年内存若不足 -
如果老年代可用内存大于历次新生代
GC
后进入老年代的对象平均大小, 但是老年代已经使用的内存空间超过了这个参数指定的比例(-XX:CMSInitiatingOccupancyFaction
), 也会自动触发FGC
(7) 多大的对象直接进入老年代?
大对象可以直接进入老年代, 因为大对象很可能是要长期存活和使用的
一般设置超过 1MB 的对象为大对象
(8) JVM
参数标准格式?
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGc -XX:+UseConcSweepGC