如何定位垃圾
1. 引用计数法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
2. 根可达算法(可达性分析算法)
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
标记-清除算法 (mark - sweep)
标记阶段,会将要回收的算法进行标记(先判断是否要进行回收)
在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
缺点
- 标记和清除过程效率都不高
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存
标记-压缩 (mark - compact)
在标记的基础上,将未标记的对象(块)(不回收的存活对象)都向一端移动,然后清除标记的块。
优点
- 不会产生内存碎片
缺点
- 需要移动大量对象,处理效率比较低
拷贝算法 ( copying )
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
- 主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
- 存放不下的对象会存放在老年代。
JVM 内存分代模型(用于分代垃圾回收算法)
部分垃圾回收器使用的模型
一般将堆分为新生代和老年代
-
新生代使用:复制算法
-
老年代使用:标记 - 清除 或者 标记 - 压缩 算法 (g1 使用 copy算法)
-
永久代 (1.7) / 元数据(1.8)
- 永久代和元数据都是存放 class 的
- 永久代必须指定大小限制。元数据可以设置也可以不设置,无上限(受限于物理内存)
- 字符串常量 1.7版本存储在 永久代,1.8版本存储在 堆 中。
- 方法区是逻辑上的概念, 1.7时 方法区对应 永久代,1.8时 方法区对应 元数据。
新生代
新生代 = 1个eden 区 + 2 个 survivor 区 ,一次YGC(对新生代堆进行gc,频率比较高)后,新生代大部分对象都将被回收,所以使用copy算法,将不回收的对象丢到 一个 survivor 中,然后清除另外一个survivor (如果有的话)和 eden。多次YGC之后,年龄足够(不同的回收器年龄不同 CMS 6 岁,有些回收器是15)的对象 进入 老年代。(拷贝不下的对象直接丢给老年代)
默认 新 new 的对象 会在 eden 区,放不下的话直接丢入 老年代 。
FGC = Minor GC
老年代
- 老年代存放的是顽固分子(一直没有被回收的),以及新生代放不下的。
- 老年代满了 FGC(Full GC = YGC + old GC ,FGC = Major GC)
GC Tuning (Generation GC调优)
- 尽量减少 FGC
垃圾回收器
-
Serial 新生代 串行回收
-
Parallel Scavenge 新生代 并行回收
-
ParNew 新生代 配合CMS的并行回收 (Parallel Scavenge 无法配合 CMS)。
-
Serial Old 老年代 串行回收
-
Parallel Old 老年代 并行回收
-
CMS(Concurrent Mark Sweep)老年代的,并发的 垃圾回收和应用程序同时运行,降低STW(Stop The World)的时间(200ms)
- CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
-
G1 (STW 10ms)
-
ZGC (STW 1ms 都可以PK c++ )
-
Shenandoah
10.Eplison
JDK1.8 默认的垃圾回收: Parallel Scavenge + Parallel Old
JVM调优第一步,了解生存环境下的垃圾回收器组合
- JVM的命令行参考
- JVM参数分类
标准 : - 开头,所以的HotSpot都支持
非标准:-X 开头 ,特定版本HotSpot支持特定命令
不稳定: -XX 开头,下个版本可能取消
比如 java -version 就是标准命令
输入 java -X 可以看到 非标准命令列表
输入 java -XX:+PrintFlagsFinal 可以设置值(最终生效值)
java -XX:+PrintFlagslnitial 默认值
java -XX:+PrintCommandLineFlags 命令行参数