垃圾:GC只会回收没有被引用或者根集不可到达的对象(取决于GC算法)
垃圾收集主要是针对堆和⽅法区进⾏。程序计数器、虚拟机栈和本地⽅法栈这三个区域属于线程私有 的,只存在于线程的⽣命周期内,线程结束之后就会消失,因此不需要对这三个区域进⾏垃圾回收。
什么时候垃圾回收?
1 Edan 或s区不够用 触发YGC
2 老年代不够用了 可能触发 Major / full 、Mixed gc
3 方法区不够用了 full gc
判断⼀个对象是否可被回收
1 引用计数法
在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。 当计数器为 0 时,就认为该对象无效了。 引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是主 流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为它很难解决对象之间循环引 用的问题。
循环引用:对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它们互相引用着对方,导致它们的引用计数都不为 0,于是 引用计数算法无法通知 GC 收集器回收它们。
2可达性分析法
以 GC Roots 为起始点进⾏搜索,可达的对象都是存活的,不可达的对象可被回收。
GC Roots 是指:
Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中引用的对象
方法区中常量引用的对象
方法区中类静态属性引用的对象
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。
引⽤类型
不同的引用类型, 主要体现的是对象不同的可达性状态 reachable 和垃圾收集的影响。
强引用(Strong Reference) 类似 "Object obj = new Object()" 这类的引用,就是强引用,只要强引用存在,垃圾收集器永远 不会回收被引用的对象。
但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量, 那么对象在很长一段时间内不会被回收,会产生内存泄漏。
软引用(So Reference) 软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内 存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清 理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂 时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用(Weak Reference) 弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收 只被弱引用关联的对象。
虚引用(Phantom Reference) 虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存 在,完全不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 以后,做某些事 情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制。为⼀个对象设置虚引⽤的唯⼀⽬的是能在这个对象被回收时收到⼀个系统通知。
垃圾收集算法
1标记-清除算法
标记的过程是:遍历所有的 GC Roots ,然后将所有 GC Roots 可达的对象标记为存活的 对象。
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除那些被标 记过的对象的标记,以便下次的垃圾回收。
另外,还会判断回收后的分块与前⼀个空闲分块是否连 续,若连续,会合并这两个分块
这种方法有两个不足:
效率问题:标记和清除两个过程的效率都不高。
空间问题:标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配 较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.复制算法(新生代) 为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每 次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,
就将存活者的对象复制到另 一块上面,然后将第一块内存全部清除。
这种算法有优有劣:
优点:不会有内存碎片的问题。
缺点:内存缩小为原来的一半,浪费空间。
为了解决空间利用率问题,可以将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。
回收时,将 Eden 和 Survivor 中还存活的对象一次 性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。
但是我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其 他内存(指老年代)进行分配担保。
分配担保
为对象分配内存空间时,如果 Eden+Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进 行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,
这样存活的对象直接通过分 配担保机制进入老年代,然后再将新对象存入 Eden 区。
3标记-整理算法(老年代)
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots ,然后将存活的 对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后直接清理掉端边界以外的内存
因此,第二阶段才称为整理阶段。
原因:这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量 对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。
优点: 不会产⽣内存碎⽚ 不⾜: 需要移动⼤量对象,处理效率⽐较低。
4分代收集算法 根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各 个年代的特点采用最适当的收集算法。
新生代:复制算法 老年代:标记-清除算法、标记-整理算法
Java新生代、老生代和永久代
新生代和老年代空间比值:1:2
https://www.jianshu.com/p/d3a0b4e36c28
https://www.bilibili.com/video/BV1NY4y1w75h?p=32&spm_id_from=pageDriver&vd_source=daaec3cbcefce2c1d58e3f9a70b93f55
堆常用设置
-Xms:初始堆大小
-Xmx:最大堆大小、
-Xmn:新生代大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/44
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。
一、为什么要分为新生代和老年代?
根据对象存活的时间来看,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。
寿命短的区清理频次高一点,寿命长的区清理频次低一点。提高效率。所以就有了新生代和老年代。
空间碎片,明明有足有的空间,但无法给对象分配足够的内存
因此新生代划分为EDAN 和s1s2
s1s2区也会有空间碎片,因此又分为s1 s2
1 新生代
主要是用来存放新生的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
新生代分为Eden区、ServivorFrom、ServivorTo三个区。
- Eden区:Java新对象的出生地(如果新创建的对象占用内存很大则直接分配给老年代)。当Eden区内存不够的时候就会触发一次MinorGc,对新生代区进行一次垃圾回收。
- ServivorTo:保留了一次MinorGc过程中的幸存者。也作为这一次GC的被扫描者。
- ServivorFrom: 既包含上一次GC的幸存者,也作为这一次GC的被扫描者。
- ServivorTo和ServivorFrom总有一个是空的
当JVM无法为新建对象分配内存空间的时候(Eden区满的时候),JVM触发MinorGc。因此新生代空间占用越低,MinorGc越频繁。
MinorGC采用复制算法。
2 老年代
-
老年代的对象比较稳定,所以MajorGC不会频繁执行。
何时进入老年代
1 对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
2 大对象直接进入老年代 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
3 s1 经过垃圾收集后把存活的对象放入s2 ,超过s2的50%。此时会把年龄最大的放入老年代
HotSpot 垃圾收集器
HotSpot 虚拟机提供了多种垃圾收集器,每种收集器都有各自的特点,虽然我们要对各个收集 器进行比较,但并非为了挑选出一个最好的收集器。我们选择的只是对具体应用最合适的收集 器。
一般新生代采用标记-复制算法,老年代采用标记-整理算法
新生代垃圾收集器
Serial 垃圾收集器(单线程):
它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
ParNew 垃圾收集器(多线程)
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
Parallel Scavenge 垃圾收集器(多线程)并行
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)
老年代垃圾收集器
Serial Old 垃圾收集器(单线程)
Serial 收集器的老年代版本,主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
Parallel Old 垃圾收集器(多线程)
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
CMS 垃圾收集器 是一种 “标记-清除”算法
是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作(并发)
冲突: 但是当某个垃圾刚要被回收时,突然又不是垃圾(又有引用指向它)|
整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫 同时产生新垃圾下次清理,这个新垃圾就是浮动垃圾
- 但是初始标记和重新标记是STW的
CMS 主要优点:并发收集、低停顿
CMS 缺点:
- 对 CPU 资源敏感;并发阶段会降低吞吐量
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生
综上
内存比较小的时候,Serial 和Serial Old 配合
比较大 时 Parallel Scavenge 和Parallel Old 配合
更大时 ParNew 和CMS 配合
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old
G1 通用垃圾收集器
G1 是一款面向服务端应用的垃圾收集器,保留新生代和老年代的概念,但不在物理隔离。而是将堆划分为一块 块独立的 Region。
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾 回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。 优先回收垃圾最多的区域
从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基 于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
1 不会内存空间碎片。
2 可预测的停顿:让使用者指定一个时间。消耗在垃圾收集的时间不超过该时间
3 保留新生代和老年代的概念,但不在物理隔离。而是将堆划分为一块 块独立的 Region.每次都从垃圾 回收价值最大的 Region 开始回收
这里抛个问题
一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描 整个堆内存才能完整地进行一次可达性分析?
并不!每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的 区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存 进行遍历。
如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:
初始标记:标记与GC roots直接关联的对象。
并发标记:可达性分析。
最终标记,对并发标记过程中,用户线程修改的对象再次标记一下。
筛选回收:对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间制定回收计划并回收。
其实CMS 和G1的前3步一样的
G1 调优
1 G1 会自动调节新生代和老年代大小。我们只要设置整个堆大小
2 不断调节暂停时间目标
3 适当的增加标记线程的数量
4 适当增大堆内存
5 不正常的Full GC
推荐G1 JDK7才有G1
超过50% 不是这次垃圾回收能回收的