jieye の 数字花园

Search

Search IconIcon to open search

垃圾回收

Last updated Unknown

# 引用类型

# 强引用

强引用是最普遍的引用,常见引用方式如下:

这种显示声明的引用,比如 objObject 对象的引用,和 strabcd 的引用都是强引用。
只要引用链没有断开(比如没有设置 obj = null),强引用就不会断开。即便内存空间不足,抛出 OutOfMemoryError 终止程序也不会回收具有强引用的对象。

# 软引用

软引用是通过 SoftReference 类来表示的。常见引用如下:

在内存不足时 JVM 会回收该对象。常见的应用场景是页面缓存、图片缓存等。

# 弱引用

弱引用通过 WeakReference 类来表示。常见引用如下:

无论内存是否充足,都会回收弱引用关联的对象。ThreadLocalThreadLocal.ThreadLocalMap 的实现就是用的弱引用。

# 虚引用

虚引用通过 PhantomReference 类来实现。

虚引用仅仅只是提供了一种确保对象被 finalize 之后来做某些失去的机制,当一个对象只有虚引用指向它时,如果 JVM 在下一次垃圾回收时决定回收该对象,就会将该虚引用加入到与之关联的引用队列中。
引用队列可以用来跟踪对象何时被回收,如果虚引用与之关联的对象被回收,就会在引用队列中添加一个通知。应用程序可以通过检查引用队列来了解对象何时被回收,并执行一些相应的操作,例如释放相关的资源或者记录相关的日志。

# 标记

# 引用计数法

这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值 +1;当引用失效时,计数器值 -1。任何时刻计数值为 0 的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java 中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。

# 可达性分析法

这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链(即 GC Roots 到对象不可达)时,则证明此对象是不可用的。

那么问题又来了,如何选取 GCRoots 对象呢?在 Java 语言中,可以作为 GCRoots 的对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中 JNI(Native 方法) 引用的对象。

# 内存分配与回收策略

# Minor GC、Major GC、Full GC

JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)

# 垃圾回收算法

# 1 标记 - 清除

image.png

将存活的对象进行标记,然后清理掉未被标记的对象。
不足:

# 2 标记 - 整理

image.png

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

# 3 复制

image.png

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

主要不足是只使用了内存的一半。

现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

# 4 分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

# 垃圾收集器

image.png

# 1 Serial 、serial Old

image.png
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。

# 2 Parnew

image.png
是 Serial 收集器的多线程版本。

是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

# 3 Parallel Scavenge 、Parallel Old

与 ParNew 一样是多线程收集器。
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。

可以通过一个开关参数打开 GC 自适应的调节策略 (GC Ergonomics),就不需要手动指定新生代的大小 (-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

# 4 CMS 收集器

image.png

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:

# 5 G1

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
image.png

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
image.png

具备如下特点:

# 理解 GC 日志

每种收集器的日志形式都是由它们自身的实现所决定的,换言之,每种收集器的日志格式都可以不一样。不过虚拟机为了方便用户阅读,将各个收集器的日志都维持了一定的共性,来看下面的一段 GC 日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)
  eden space 2176K,   2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)
  from space 256K,   0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)
  to   space 256K,   0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)
 tenured generation   total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)
   the space 5312K,   3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)
 compacting perm gen  total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)
   the space 21248K,  14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000)
No shared spaces configured.

1、日志的开头“GC”、“Full GC”表示这次垃圾收集的停顿类型,而不是用来区分新生代 GC 还是老年代 GC 的。如果有 Full,则说明本次 GC 停止了其他所有工作线程 (Stop-The-World)。看到 Full GC 的写法是“Full GC(System)”,这说明是调用 System.gc() 方法所触发的 GC。

2、“GC”中接下来的“[DefNew”表示 GC 发生的区域,这里显示的区域名称与使用的 GC 收集器是密切相关的,例如上面样例所使用的 Serial 收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是 ParNew 收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用 Parallel Scavenge 收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

3、后面方括号内部的“310K->194K(2368K)”、“2242K->0K(2368K)”,指的是该区域已使用的容量 ->GC 后该内存区域已使用的容量 (该内存区总容量)。方括号外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”则指的是 GC 前 Java 堆已使用的容量 ->GC 后 Java 堆已使用的容量 (Java 堆总容量)。

4、再往后“0.0269163 secs”表示该内存区域 GC 所占用的时间,单位是秒。最后的“[Times: user=0.00 sys=0.00 real=0.03 secs]”则更具体了,user 表示用户态消耗的 CPU 时间、内核态消耗的 CPU 时间、操作从开始到结束经过的墙钟时间。后面两个的区别是,墙钟时间包括各种非运算的等待消耗,比如等待磁盘 I/O、等待线程阻塞,而 CPU 时间不包括这些耗时,但当系统有多 CPU 或者多核的话,多线程操作会叠加这些 CPU 时间,所以如果看到 user 或 sys 时间超过 real 时间是完全正常的。

5、“Heap”后面就列举出堆内存目前各个年代的区域的内存情况。