什么是GC?
GC的本质: 从GC Roots开始,沿着引用链找到所有的可以到达的对象(reachable objects),并把它们标记为活动对象(alived objects),这个过程叫可达性分析。
垃圾回收器跟踪所有仍在使用的对象,并将其余对象标记为垃圾。
内存管理的两个流派
手动管理内存(C/C++)
正如我们所看到的,很容易忘记释放内存。内存泄漏是常见的问题。只有修改代码才能真正解决问题。因此,更好的方法是自动回收未使用的内存,完全消除人为错误的可能性。这种自动化称为垃圾收集(简称GC)
自动内存管理(几乎所有的现代编程语言)
引用计数(
Reference Counting)绿色的云表明程序员指向的对象仍在使用中。 从技术上讲,这些可能是诸如当前正在执行的方法中的局部变量或静态变量之类的东西
蓝色圆圈是内存中的活动对象,其中的数字表示其引用计数。
灰色圆圈是尚未从仍在显式使用的任何对象中引用的对象(这些对象由绿云直接引用)。 因此,灰色物体是垃圾,可以由垃圾收集器清理
引用计数的致命缺点—-循环引用
由于循环引用,其引用计数不为零,红色对象实际上是应用程序不使用的垃圾。但是由于引用计数的限制,仍然存在内存泄漏
标记清除(Mark and Sweep)
从一组预先定义好的根节点(GC root)开始,把所有可达的对象都打上标记,然后把不可达的对象(垃圾)全部清除掉。JVM用来跟踪所有可访问(活动)对象并确保不可访问对象声明的内存可以重用的方法称为标记和清除算法。
标记清除主要分为两步:- Marking: 从
GC Roots开始遍历所有可访问的对象,并在本机内存中保存这些对象的有关记录 - Sweeping: 清除不可达对象占用的内存地址,以确保下一次可以重新分配这些内存
- Marking: 从
碎片化问题
长期工作的内存会出现碎片化,总可用空间仍然足够,但是无法分配大对象
每当进行垃圾清理时,JVM必须确保这些垃圾对象所占的内存可以被重用,但是这可能会导致内存碎片化问题,这和磁盘碎片类似,会导致两个问题:
- 创建新对象时,
JVM会分配连续的内存,因此如果碎片化到达某种程度,导致没有任何单独的空闲碎片小都不足以容纳新创建的对象,则会发生分配错误 - 内存的
Write操作,会因为很难找到下一个足够大的内存块,而变的更加耗时
为了避免这些问题,JVM会确保碎片化不会到失控的地步,因为垃圾回收不仅要把垃圾清除掉,还要做内存碎片整理,把剩下的非垃圾对象所占的内存压缩的更紧密(内存地址更连续),方便后续分配较大的对象。
Java中的引用类型
- 强引用 Strong Reference : 你用到的都是强引用
- 软引用 Soft Reference: 内存不够的时候会回收
- 弱引用 Weak Reference: GC碰到它们就会回收
- 影子引用 Phantom Reference: 只能拿到影子,拿不到引用本身
对象的分代假设
研究表明,绝大多数对象的生命周期都很短(朝生夕死)
内存分代
有了这些独立的、单独的可清理区域,就可以使用大量不同的算法,这些算法在提高GC性能方面取得了长足的进步
- 年轻代 (Young Generation)
- `Eden`: 伊甸园
- `Survivor`: 幸存区
- 老年代 (Old Generation/Tenured)
- 永久代(Permanent Generation)
任何新创建的对象都会先进入Eden(上帝创造亚当夏娃,让他们结为夫妻生活在伊甸园), 然后经历了一场洪水或灾难之后,进入了Survivor (幸存区),当一个对象经历过很多灾难(GC)后,它会变得很资深,就进入了Tenured(老年代)
年轻代
Eden(伊甸园)- 可以分为多个 Thread Local Allocation Buffer
Java中新创建的对象会分配到Eden中。由于会有多个线程同时创建多个对象的场景,因此Eden 进一步分为一个或多个驻留在Eden空间中的Thread Local Allocation Buffer 简称(TLAB)。 这些缓冲区允许JVM在相应的TLAB中直接在线程内分配大多数对象,从而避免了和其它线程的同步问题。
当TLAB中没有足够的空间时,该对象会被分配到Eden的共享空间(如上图中的Common area), 此时会触发Young GC(年轻代的垃圾回收)来释放更多的空间,如果垃圾回收后仍没有足够的空间,这些对象会被放入 老年代中。
当Eden发生GC时,GC会从Gc Roots开始,沿着引用链访问所有可达对象,并将其标记为活动对象。
Marking phase(标记阶段)完成后,Eden中的所有活动对象会被复制到一个Survivor中,此时整个Eden被认为是空的,可以用来重新分配对象。
这种方法叫做"Mark And Copy": 标记活的对象,然后把他们移动到Survivor中去。
Survivor(幸存区)- `Young GC`发生时,整个年轻代中的对象会进入其中一个`Survivor`区
- 足够老的对象会提升到老年代
紧挨着Eden的是两个叫做fromandto的生存空间。需要注意的是,两个Survivor中的一个总是空的。
在两个Survivor之间复制活动对象的过程会重复几次,直到某些对象被认为已经足够成熟, 就可以晋升到 Tenured(老年代)
如何判断活动对象是否"足够老"到可以晋升到Tenured?
在每一代对象完成一个GC之后,那些仍然活着的对象的年龄就会增加。每当年龄超过某个阈值时,对象将被提升到老年代
实际的tenuring阈值由JVM动态调整,但指定-XX:+maxtenuringthreshold设置上限。设置- xx:+MaxTenuringThreshold=0 会导致活动对象由Eden直接晋升到老年区,无需在Survivor之间来回复制活动对象。默认情况下,现代JVM上的这个阈值设置为15个GC周期。这也是HotSpot中的最大值。
老年代
- 老年代相对较大
- 存储足够年老的对象(经历过15次GC)
- 发生GC的频率较低
- 清除垃圾
- 压紧内存
永久代/元空间
Java8之前:永久代
是堆得一部分
存储类数据、字符串常量
java.lang.OutOfMemoryError:Permgenspacejava -XX:MaxPermSize=256m com.mycompany.MyApplication
Java8之后:元空间
不是堆得一部分
除非特殊指定,否则空间大小没有上限
java.lang.OutOfMemoryError:Metaspacejava -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication
GC的种类
Minor GC/Young GC
- 在
JVM无法为新对象分配空间时触发,例如Eden变得满了。因此,分配率越高,发生Minor GC的频率就越高 - 会触发STW(暂停应用程序线程)
- 在
Major GC vs Full GC
- 这两个概念没有明确的定义,这里阐述一般理解
- Major GC 清除老年代
- Full GC 清除整个堆--包括年轻代和老年代
- 这两个概念没有明确的定义,这里阐述一般理解
GC的过程
标记阶段(Marking Reachable Objects)
JVM中使用的所有现代GC算法都是从找到所有alive objects(活动对象)开始。
下面这张图很好的解释了JVM的内存分布(绿色云代表GC Roots, 蓝色圈代表可达对象,灰色圈代表不可达对象):
首先GC定义了一些指定的对象作为GC Roots:
- 活的线程
- 类的静态成员
- 线程方法栈索所引用的对象
- JNI引用的对象
- 分代GC时其它代的对象
然后从GC Roots开始沿着引用链做可达性分析,找到所有可达对象并将它们标记为活动对象(alive objects)。
标记阶段有几个重要方面需要注意:
- 上面说到可达性分析,但是分析肯定需要时间,所以GC需要线程停止才能开始收集垃圾, 因为线程不停止,引用将一直在变动,就不能准确的计数或标记对象。当线程被临时停止,JVM就可以进行管理活动了,这种情况叫Stop The World ,简称 STW(举个例子:就像水塘里有活鱼有死鱼,你要找到所有的活鱼并标记,但如果鱼一直在游动,你可能永远也标记不完)
- 这个暂停的持续时间既不取决于堆中对象的总数,也不取决于堆的大小,而是取决于alive objects的数量。因此,增加堆的大小不会直接影响标记阶段的持续时间
清除阶段(Removing Unused Objects)
不同的GC算法对Unused Objects对象的删除有所不同,但可以分为三类:
- Sweep
- Compact
- Copy
Sweep(Mark and Sweep): 标记-清除
Compact(Mark-Sweep-Compact):标记-清除-压缩
老年代中使用
Copy(Mark-and-Copy): 标记-清除-复制
年轻代中使用(Eden—>Survivor)
所以一个完整的GC过程如下:
- STW (Stop The World)
- 清除垃圾
- 压缩内存/复制到另一块内存