说到 Java 很多人脑海会直接蹦出内存自动回收,会经常听到 GC 这些词,GC 指的是 Garbage Collection 也就是垃圾回收。说到垃圾回收就不可避免的去看下 Java 的内存管理机制。

内存管理

提到内存管理可能很多人都会联想起 C/C++ 的手动管理,以及 Java/Python 的自动管理,但实际上这都是指的堆内存管理。常规的内存管理可以分成两个部分,栈内存的管理和堆内存管理。

栈的发明解决了部分内存的自动回收,但是栈的局限在于只能自动管理固定长度的内存。而堆内存不同语言有不同的管理方式:

  • 存手动管理 C/C++
  • 自动管理 Java Python PHP C# 等
  • 半自动 C++ 智能指针 Swift Rust 等

什么是 GC 以及为何要有 GC

对于通常含以上的 GC 可以参考维基词条, John McCarthy 在发明 Lisp 时一并发明了内存自动管理机制。

什么是内存垃圾,也就是程序在执行过程中再无法访问的对象,这些对象所占用的内存空间可以收回来重新使用。

优点:

  • 编码容易
  • 减少因内存管理而导致的 bug,野指针,内存泄露等等

GC 的缺点:

  • 需要消耗额外的 CPU / Memory 资源
  • 代码执行时间无法估计

GC 实现方式

Reference counting

引用计数,最简单也最容易实现的一种,原理是在每个对象中保存该对象的引用计数,当引用发生增减时对计数进行更改。

优点:

  • 当对象不再被引用立即就会被释放,算法运行快
  • 空间释放是针对个别执行,和其他算法相比,GC 中断时间比较短

问题:

  • 额外的内存占用,每个对象需要一个 counter
  • 引用发生增减时需要对计数做出正确的增减,如果发生计数错误可能会导致难以发现的内存错误
  • 循环引用,两个对象互相引用,能解决但需要大量计算
  • 引用计数不适合并行处理,多线程同时对引用计数进行增减时,引用计数可能会产生不一致的问题,而如果采用加锁方式,带来的开销也非常大

引用计数的例子:

  • Python
  • PHP
  • Swift/ OC

Mark and Sweep

标记清除,是最早开发出来的算法(1960 年),原理,从根开始可能被引用的对象用递归的方式进行标记,然后将没有标记的对象作为垃圾回收。

缺点:

  • 在分配了大量对象,而只有一小部分存活的情况下,算法消耗的时间多
  • 执行时间不可控

Copy and Collection

复制收集,为了克服标记清除的问题,在算法中将根开始被引用的对象复制到另外的空间中,然后将复制的对象所能够引用的对象用递归方式复制下去。

复制收集方式的过程相当于标记清除算法中的标记阶段,但由于清除阶段依然要对所有对象进行扫描,如果存在大量对象,而且大量对象已经死亡的情况,开销会加大。

优点:

  • 没有内存碎片
  • 复制收集过程中,按照对象被引用的顺序将对象复制到新空间,关系较近的对象被放到较近空间的可能性提高,局部性能提升,内存缓存可能更容易命中

缺点

  • 和标记清除相比,复制对象的开销加大,当存活对象较多的情况下,性能损耗

JVM 如何实现 GC

现代版的 GC 使用分代收集,按照对象存活时间长短来使用不同的垃圾回收算法。

heap 分为:

  • Young Generation: 新创建的对象,Young Generation 又被分成 Eden space(所有新对象开始的地方),两个 Survivor spaces(在存活一个 gc 之后移动到这里),当对象在 Young Generation 被回收,这是 minor garbage collection event
  • Old Generation: 当对象存活足够长时间,会从 Young Generation 移动到 Old Generation。当对象在 Old Generation 被回收,这是一次 major garbage collection event.
  • Permanent Generation: 类,方法等 Metadata 会保存在 Permanent Generation。不再被使用的 Classes 会被回收。

所以一个对象在不同分区的流程可能是:

  • 新对象在 Eden 中创建
  • Eden 满时进入 Survivor spaces
  • 两个 Survivor 空间的对象相互交换
  • 在存活一定时间后进入 Old

一种 JVM 的实现,由 1999 年引入, HotSpot 为代表,HotSpot JVM 有四种 Garbage Collector:

  • Serial GC : 所有的 garbage collection events 通过一个线程连续管理, Compaction 在每一次 garbage collection 之后执行
  • Parallel GC : 并行进行 minor garbage collection,一个线程用来 major garbage collection 和 Old Generation compaction
  • Concurrent-Mark-and-Sweep GC : 简称 CMS,多个线程用来 minor garbage collection ,使用和 Parallel 相同的算法。CMS 在应用程序之外运行,GC 和 应用程序并行,减少中断。不会执行 compaction
  • G1 GC(1.7+) : Garbage First,新的 garbage collector,用来替换 CMS,同样是并行并发的,但是原理和工作方式完全不一样

Java 的 GC 是不确定的,没有方法来预测何时会发生 gc。在代码中可以使用 System.gc() 或者 Runtime.gc() 方法来暗示 gc,但是 Java 不能保证 gc 一定会执行。

GC 的调优可以从 JVM 的参数调节

  • -Xms 初始堆大小
  • -Xmx 最大堆大小
  • -XX:NewRatio=n Young 和 old 区的大小比 1:n
  • -XX:MaxPermSize Permanent 大小
  • -XX:+UseG1GC 使用 G1
  • -XX:MaxGCPauseMillis=n 最大希望暂停时间
  • -XX:InitiatingHeapOccupancyPercent=n 堆使用到多少百分比时开始 CMS 过程
  • -XX:+PrintGC 输出 GC 日志
  • -XX:+PrintGCDetails 输出 GC 的详细日志
  • -XX:+PrintGCTimeStamps 输出 GC 时间戳
  • -XX:+PrintGCDateStamps 输出 GC 时间戳(日期形式,2019-01-01T01:01:02.212+0800)
  • -XX:+PrintHeapAtGC 进行 GC 的前后打印堆信息
  • -Xloggc:../logs/gc.log 日志文件输出路径

相关命令

jstat

jstat [options] vmid [interval] [count]
  • options 使用 -gc 或者 -gcutil
  • 这里的 vmid 是 vm 的进程号,当前运行的 java 进程号

比如查看 gc 情况

jstat -gc [PID]

每 5 秒打印一次

jstat -gc [PID] 5000

结果说明

S0C:年轻代中第一个 survivor(幸存区)的容量 (字节) 
S1C:年轻代中第二个 survivor(幸存区)的容量 (字节) 
S0U:年轻代中第一个 survivor(幸存区)目前已使用空间 (字节) 
S1U:年轻代中第二个 survivor(幸存区)目前已使用空间 (字节) 
EC:年轻代中 Eden(伊甸园)的容量 (字节) 
EU:年轻代中 Eden(伊甸园)目前已使用空间 (字节) 
OC:Old 代的容量 (字节) 
OU:Old 代目前已使用空间 (字节) 
PC:Perm(持久代)的容量 (字节) 
PU:Perm(持久代)目前已使用空间 (字节) 
YGC:从应用程序启动到采样时年轻代中 gc 次数 
YGCT:从应用程序启动到采样时年轻代中 gc 所用时间 (s) 
FGC:从应用程序启动到采样时 old 代(全 gc)gc 次数 
FGCT:从应用程序启动到采样时 old 代(全 gc)gc 所用时间 (s) 
GCT:从应用程序启动到采样时 gc 用的总时间 (s) 
NGCMN:年轻代 (young) 中初始化(最小)的大小 (字节) 
NGCMX:年轻代 (young) 的最大容量 (字节) 
NGC:年轻代 (young) 中当前的容量 (字节) 
OGCMN:old 代中初始化(最小)的大小 (字节) 
OGCMX:old 代的最大容量 (字节) 
OGC:old 代当前新生成的容量 (字节) 
PGCMN:perm 代中初始化(最小)的大小 (字节) 
PGCMX:perm 代的最大容量 (字节)   
PGC:perm 代当前新生成的容量 (字节) 
S0:年轻代中第一个 survivor(幸存区)已使用的占当前容量百分比 
S1:年轻代中第二个 survivor(幸存区)已使用的占当前容量百分比 
E:年轻代中 Eden(伊甸园)已使用的占当前容量百分比 
O:old 代已使用的占当前容量百分比 
P:perm 代已使用的占当前容量百分比 
S0CMX:年轻代中第一个 survivor(幸存区)的最大容量 (字节) 
S1CMX :年轻代中第二个 survivor(幸存区)的最大容量 (字节) 
ECMX:年轻代中 Eden(伊甸园)的最大容量 (字节) 
DSS:当前需要 survivor(幸存区)的容量 (字节)(Eden 区已满) 
TT: 持有次数限制 
MTT : 最大持有次数限制 

如果使用 -gcutil 则是打印 GC 的使用率:

jstat -gcutil -h 10 [PID] 1000

jmap

使用 jmap 打印堆相关信息,更多细节可以参考这篇文章

jhat

更多关于 jhat 的用法可以参考这篇

reference