GC 随java 流行而被熟知,最早起源于LISP,一种自动内存管理机制

回收什么? what

清理的是垃圾,回收的是内存

jvm 将所管理内存划分为区域

  • 堆(Heap)
  • 方法区(Method Area)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method Stack)
  • 程序计数器(Program Counter Register)

堆、方法区 所有线程共享,其他属于线程隔离的区域

堆(Heap)

一般堆是最重要的一块考点 :)

收集器基本都用分代收集算法

由 young+old+permanent组成,JVM又进一步将Young分成了eden,from survivor(又名Survivor 0, s0)和to survivor(又名Survivor 1,s1)三个区域

Eden、S0、S1 组成新生代(Young/New Generation)

Old/Tenured 为老年代

新对象会先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)

在GC中,Eden 中的对象会被移动到survivor中,直至对象熬过一定的GC的次数,会被移动到老年代

老年代一般是一些系统级(线程库,classloader等)的对象

官方推荐新生代占堆大小的3/8,survivor区各占新生代的1/10

一般只在新生代(的Eden)分配内存简化了新对象的分配

新生代和老年代使用不同的GC算法,可以更有效的清除不再需要的对象

很多对象的生存时间都很短,而新生对象很少引用生存时间长的对象。所以,GC会频繁访问新生代对象,执行Minor GC

新生代中,GC可以快速标记回收”死对象”,而不需要扫描整个Heap中的存活一段时间的”老对象”(即执行major/FULL GC)

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC

对老年代GC称为Major GC,而Full GC是对整个堆来说的

最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的

Major GC的速度一般会比Minor GC慢10倍以上。下边看看有那种情况触发JVM进行Full GC及应对策略


新生代GC用复制算法

GC前To survivor保持清空,对象保存在Eden和From survivor区中

GC运行时,Eden中的幸存对象被复制到 To survivor区

针对 From survivor取中的幸存对象,年龄没达到阀值(tenuring threshold),复制到To survivor区

达到阀值,复制到老年代。复制完成后,Eden 和From survivor区中只保存死对象,可全部清空

如果复制过程中To survivor满了,剩余对象复制到老年代中

最后 From和To会对换

黄色死对象,绿色剩余空间,红色幸存对象

新生代过小,导致新生对象很快就晋升到老年代中,老年代中对象很难被回收

新生代过大,发生过多的复制过程

所以需要通过不断的测试调优,找到一个合适的JVM参数

方法区(Method Area)

虚拟机加载的类信息、常量、编译代码等数据

HotSpot 虚拟机使用永久代(Permanent Generation)来实现方法区

虚拟机栈(VM Stack)

描述java 方法执行内存模型,每个方法在执行时创建一个栈帧(Stack Frame)

栈帧中存储内容主要包含:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址

每个方法的执行过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

本地方法栈(Native Method Stack)

与虚拟机栈类似

HotSpot 实现将两者合二为一

程序计数器(Program Counter Register)

看作线程执行的字节码行号指示器,便于实现分支、循环、跳转、异常处理和线程切换恢复等基础功能

每线程独立

GC管理的内存区域主要是堆(Heap),而堆中存放的是对象实例,因此 GC 回收的就是“死亡”(不可能再被使用)的对象占用的内存空间

何时回收?when

“死亡”对象的生命周期

虚拟机通过 new 指令创建了对象,大多在 Eden 配内存,大对象若 Eden不满足直接在 Old/Tenured 区分配

对象死亡判定,主流 GC 实现通过可达性分析,形象点来说就是在基于引用建立的对象图中形成了孤岛的对象就是死亡的(可回收的)

三类 GC

Minor/Major/Full GC

Minor GC 新生代回收,Eden 区满触发 Minor GC

Major GC 老年代回收,Minor GC 发生时会拷贝对象到老年代,过程称为对象晋升(promotion)或老年化(tenuring

为避免对象晋升时老年代空间不足,收集器总是尝试预测剩余的空间是否足够以避免对象晋升失败,当晋升失败时就会发生 Full GC

Full GC 是针对整个堆的操作,操作非常昂贵

除了在对象晋升失败时发生 Full GC,当堆自动调整大小时(Heap-Resizing)也会发生,设置 -Xms和-Xmx相同值可避免 Heap-Resizing

如何回收? how

Minor GC 将新生代中存活的对象拷贝到 Survivor 区和 Tenured 区

Major GC 针对老年代区域进行死亡对象标记、清除和内存整理

Full GC 包括了所有存活对象的晋升以及老年代的内存回收及整理

具体GC过程由垃圾收集器实现


几种收集器

  • Serial

串行单线程收集器,目前虚拟机运行在 client 模式下的默认新生代收集器

  • ParNew

相当于 Serial 的多线程版本

  • Parallel Scavenge

与 ParNew 很像,但关注点在达到一个可控制的吞吐量(Throughput)

这里吞吐量的定义是 CPU 用于运行用户代码的时间与 CPU总消耗时间的比值

常称为吞吐优先收集器,还有个特点是自适应调节策略

jvm 根据os运行情况收集监控信息,动态调整 Eden与Survivor区比例、晋升老年代对象年龄等参数,以提供最合适的停顿时间或最大的吞吐量

  • Serial Old

相当于 Serial 收集器的老年代版本

  • Parallel Old

相当于 Parallel Scavenge 收集器的老年代版本


  • Concurrent Mark Sweep (CMS)

上述gc执行时都会停止所有的用户线程执行(Stop-The-World)

CMS 关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,让gc和用户线程并行执行,减少应用停顿时间,提升用户体验

低停顿得付出吞吐量的代价,通常与 Parallel 系收集器相比吞吐率下降 10%-40%

CMS

步骤

初始标记:找到 GC Roots

并发标记:标记所有从 GC Roots 可达的对象

并发预清理:检查对象引用更新和在并发标记阶段晋升到老年代的对象并进行标记

重新标记:标记预清理阶段更新的对象引用

并发清理:回收死亡对象的内存

并发重置:重置数据结构为下次运行作准备

1、4仍需 Stop The World,只是相对来说时间较短

缺点

CMS 优点低停顿,但并不完美,3 个明显缺点:

和用户线程并发执行,存在 CPU 争抢的问题

无法回收浮动垃圾

CMS 仅进行了标记、清除而未进行整理,容易产生大量内存空间碎片

CMS 默认启动的回收线程是 (CPU数量 + 3) / 4,也就是 CPU 在 4 个以上时并发回收线程使用的 CPU 资源不少于 25%

并发清理时新产生的垃圾称为浮动垃圾(Floating Garbage),本次无法收集,当浮动垃圾过多导致预留的内存无法满足程序需要时触发, 就可能出现 Concurrent Mode Failure 导致启用 Serial Old 收集器作为后备进行 Full GC

G1

Garbage First ,jdk7u4 正式支持

多分区的堆组织方式 G1 也是分代收集器,组织堆的方式与其他收集器完全不同

按不同用途将堆分为大量(~2000)固定大小的区域(region)

相同用途的堆也并不连续,G1 依然保留了新生代和老年代概念,但新生代和老年代不再是物理上隔离的了,它们都是一部分 region 的集合

如果一个对象大小超过了普通区域大小的50%,被分配到一个大区域(humongous)里面

G1追求低停顿,并且建立可预测的停顿时间模型(M 毫秒内,GC 时间不得超过 N 毫秒,N < M)

G1 通过有计划的避免在整个堆全区域扫描进行GC,通过跟踪各个 region 中垃圾的价值大小(回收获得的空间及所花时间的经验值), 后台维护优先级列表,每次根据允许的收集时间,优先回收价值最大的 region,也正是Garbage-First 名称的由来

region收集用Stop-The-World方式,增量将存活的对象复制到一个空 region 里面,不会产生内存碎片问题

谈论gc时,主要考虑三个收集器的指标:

  • 吞吐量

花费在 GC 上的时间占整个应用程序工作的比例

  • 延迟

因为垃圾回收,而引起的响应暂停的时间

  • 内存

我系统使用内存来存储状态,在管理的时候它们常常需要复制和移动

吞吐量越大越好,延迟越低越好,内存复制和移动产生的碎片越少越好

三个目标很难同时满足,很多时候都是根据应用类型在其中做出权衡取舍