https://blog.csdn.net/qq_32641659/article/details/88030753

什么是垃圾

显式的把某个引用赋值为 null或者指向新的对象
局部所引用的对象,比如循环中
弱引用与其关联的对象

内存泄漏和内存溢出

内存泄漏的定义:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。

什么是垃圾回收机制?

不定时的去堆内存清理不可达的对象

GC分类- Minor / Major / FULL

Minor GC(YGC): 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC
Major GC : 对老年代GC
Full GC : 是对整个堆来说的,出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。

1. YGC是什么时候触发的?
大多数情况下,对象直接在年轻代中的Eden区进行分配,如果没有足够的空间,那么就会触发YGC(Minor GC)
YGC处理的区域只有新生代。因为大部分对象在短时间内都是可收回掉的,因此YGC后只有极少数的对象能存活下来,而被移动到S0区(采用的是复制算法)。
当触发下一次YGC时,会将Eden区和S0区的存活对象移动到S1区,同时清空Eden区和S0区。
当再次触发YGC时,这时候处理的区域就变成了Eden区和S1区(即S0和S1进行角色交换)。
每经过一次YGC,存活对象的年龄就会加1。
2. FULL GC(何时 Full GC)

  1. System.gc()方法,只是建议而非一定,但很多情况下它会触发 Full GC 建议能不使用此方法就别使用
  2. 老年代空间不足

尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

  1. 堆中分配很大的对象

所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
(-XX:PretenureSizeThreshold=3145728 这个参数来定义多大的对象直接进入老年代)

这里必须点出一个很重要的误区:
不可达的对象并不会马上就会被直接回收,而是至少要经过两次标记的过程。
第一次被标记过的对象,会检查该对象是否重写了finalize()方法。如果重写了该方法,则将其放入一个F-Query队列中,否则,直接将对象加入“即将回收”集合。
在第二次标记之前,F-Query队列中的所有对象会逐个执行finalize()方法,但是不保证该队列中所有对象的finalize()方法都能被执行,这是因为JVM创建一个低优先级的线程去运行此队列中的方法,很可能在没有遍历完之前,就已经被剥夺了运行的权利。那么运行finalize()方法的意义何在呢?这是对象避免自己被清理的最后手段:如果在执行finalize()方法的过程中,使得此对象重新与GC Roots引用链相连,则会在第二次标记过程中将此对象从F-Query队列中清除,避免在这次回收中被清除,恢复成了一个“正常”的对象。但显然这种好事不能无限的发生,对于曾经执行过一次finalize()的对象来说,之后如果再被标记,则不会再执行finalize()方法,只能等待被清除的命运。
之后,GC将对F-Queue中的对象进行第二次小规模的标记,将队列中重新与GC Roots引用链恢复连接的对象清除出“即将回收”集合。所有此集合中的内容将被回收。

  1. FGC又是什么时候触发的
    下面4种情况,对象会进入到老年代中:
  • YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。
  • 经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
  • 动态年龄判定规则,To Survivor区中相同年龄的对象,如果其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
  • 大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。

当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FGC(Major GC),FGC处理的区域同时包括新生代和老年代。除此之外,还有以下4种情况也会触发FGC:

  • 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发FGC。
  • 空间分配担保:在YGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC。
  • Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。
  • System.gc() 或者Runtime.gc() 被显式调用时,触发FGC。
  1. 清楚从程序角度,有哪些原因导致FGC?
  • 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
  • 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
  • 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)
  • 程序BUG导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发FGC,最后导致OOM.
  • 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
  • JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。

finalize()作用

finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。


1 java的GC只负责内存相关的清理,所有其它资源的清理必须由程序员手工完成。要不然会引起资源泄露,有可能导致程序崩溃。

2 调用GC并不保证GC实际执行。

3 finalize抛出的未捕获异常只会导致该对象的finalize执行退出。

4 用户可以自己调用对象的finalize方法,但是这种调用是正常的方法调用,和对象的销毁过程无关。

5 JVM保证在一个对象所占用的内存被回收之前,如果它实现了finalize方法,则该方法一定会被调用。Object的默认finalize什么都不做,为了效率,GC可以认为一个什么都不做的finalize不存在。

6 对象的finalize调用链和clone调用链一样,必须手工构造。

7 提醒jvm垃圾回收,但不是立即执行,取决于垃圾回收制

  1. System.gc();

8 Object类finalize方法,在垃圾回收之前执行,不是100%执行,当对象=null时,执行概率增大

1. 识别垃圾

  1. 引用计数法(Reference Counting):

对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。
虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。

如何解决循环引用问题?

只靠强引用计数方式,会存在循环引用的问题,导致对象永远无法被释放,弱引用就是专门用来解决循环引用问题的: 「若 A 强引用了 B,那 B 引用 A 时就需使用弱引用,当判断是否为无用对象时仅考虑强引用计数是否为 0,不关心弱引用计数的数量」 这样就解决了循环引用导致对象无法释放的问题,但这会引发野指针问题:当 B 要通过弱指针访问 A 时,A 可能已经被销毁了,那指向 A 的这个弱指针就变成野指针了。在这种情况下,就表示 A 确实已经不存在了,需要进行重新创建等其他操作

  1. 可达性分析法⭐️,又称引用链法(Tracing GC):

从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。
目前 Java 中主流的虚拟机均采用此算法。

以下对象会被认为是root对象:
(1) 虚拟机栈(栈帧中本地变量表)中引用的对象
(2) 方法区中静态属性引用的对象
(3) 方法区中常量引用的对象
(4) 本地方法栈中Native方法引用的对象

  1. /**
  2. * 情况4是线程的start方法
  3. */
  4. public class GCRootsDemo {
  5. private byte[]bytes = new byte[1024];
  6. //情况2:方法区中的类静态属性引用的对象
  7. private static SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  8. //情况3:方法去中常量引用的对象
  9. private static final SimpleDateFormat sdf2 =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  10. public static void method(){
  11. //情况1:这里的t1是方法区栈帧中引用的对象, t1指向new GCRootsDemo()
  12. GCRootsDemo t1 = new GCRootsDemo();
  13. System.gc();
  14. }
  15. public static void main(String[] args) {
  16. method();
  17. }

2. 回收垃圾算法

1. 标记-清除(Mark—Sweep)
被誉为现代垃圾回收算法的思想基础。
image.png
(1)思想:标记清除算法分为两个阶段,

  • 追踪(Tracing)阶段,即从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象
  • 第二阶段为清除(Sweep)阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。整个算法在不同的实现中会使用三色抽象(Tricolour Abstraction)、位图标记(BitMap)等技术来提高算法的效率,存活对象较多时较高效。

标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
(2)优缺点:实现简单,容易产生内存碎片
2. 复制算法(Copying)(新生代s0/s1)

垃圾回收机制(GC) - 图2

一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
(2)优缺点:不容易产生内存碎片, 可用内存空间少;建立在垃圾对象多的前提下
空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高。

3. 标记-整理算法(标记-压缩,Mark-Compact,标记清除压缩)
垃圾回收机制(GC) - 图3
此算法是结合了“标记-清除”和“复制算法”两个算法的优点。
避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。
(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下

总结: 分代回收策略(Generational Collecting)
(新生代|老年代)
基于这样的事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

由于老年代每次只回收少量的对象,因此采用mark-compact算法

总结:
新生代:复制算法(新生代回收的频率很高,每次回收的耗时很短,为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表(Card Table)的数据结构,卡表为一个比特位集合,每个比特位可以用来表示老年代的某一区域中的所

Card Table:中文翻译为卡表,主要是用来标记卡页的状态,每个卡表项对应一个卡页。当卡页中一个对象引用有写操作时,写屏障将会标记对象所在的卡表状态改为 dirty,卡表的本质是用来解决跨代引用的问题。具体怎么解决的可以参考 StackOverflow 上的这个问题 how-actually-card-table-and-writer-barrier-works,或者研读一下 cardTableRS.app 中的源码。

新生代 ParNew Serial Parallel Scavenge
老年代 CMS SerialOld ParallelOld

垃圾回收器(分代)

2.5.1 分代收集器

  • ParNew:一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
  • CMS:以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除,详情可见 JEP 363。

2.5.2 分区收集器

  • 垃圾回收机制(GC) - 图4
  • 新生代收集器
    • Serial
    • ParNew
    • parallel
  • 老年代收集器
    • Serial Old
    • CMS
    • Parallel Old
  • 新生代和老年代收集器
    • G1

image.png

1.串行收集器 -XX:+UseSerialGC

新生代和老年代都使用 串行回收器
新生代使用复制算法,老年代使用标记-整理算法

  • 优势:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
  • 缺点:会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。
  • 适用场景:Client 模式(桌面应用);单核服务器。

    2.并行收集器

    这是JVM默认的收集器,最大的优点是使用多个线程来扫描和压缩堆。
    缺点是在minor和full GC的时候都会暂停应用的运行。并行收集器最适合用在可以容忍程序停滞的环境使用,它占用较低的CPU因而能提高应用的吞吐(throughput)。

  • 使用方法:-XX:+UseParNewGC:在新生代使用并行收集器

-XX:+UseParallelGC :新生代使用并行回收收集器,更加关注吞吐量
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数

  • 优点:随着cpu的有效利用,对于GC时系统资源的有效利用有好处。
  • 缺点:和Serial是一样的。
  • 适用场景:ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器。因为CMS收集器只能与serial或者parNew联合使用,在当下多核系统环境下,首选的是parNew与CMS配合。ParNew收集器也是使用CMS收集器后默认的新生代收集器。也可以使用如下命令进行强制指定。

    3.常用[重点] CMS收集器 -XX:+UseConcMarkSweepGC

    使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿。

Concurrent-Mark-Sweep的缩写,并发的标记与清除。
(Stop the World, STW)

目的: 一种以获取最短回收停顿时间为目标的收集器,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。

  • 它是和应用程序线程一起执行,相对于Stop The World来说虚拟机并发的方式适度减少吞吐量,减少用户线程停顿时间。
  • 它使用的是 标记清除算法,它是老年代的收集算法,新生代使用ParNew收集算法。默认关闭

缺点

  1. CMS收集器的缺点是对服务器CPU资源较为敏感,在并发标记时会降低吞吐量。它使用的标记清除算法也会产生大量空间碎片,空间碎片的存在会加大Full GC的频率,虽然老年代还有足够的内存,但是因为内存空间连续,不得不进行Full GC进行compact。
  2. CMS无法处理浮动垃圾。在并发清除阶段,用户程序继续运行,可能产生新的内存垃圾,这一部分垃圾出现在标记过程之后,因此,CMS无法清除。这部分垃圾称为“浮动垃圾“
  3. 基于标记-清除算法,容易产生大量内存碎片,导致full GC(full GC进行内存碎片的整理)


    1、initial-mark 初始标记(CMS的第一个STW阶段)
    标记GC Root直接引用的对象,GC Root直接引用的对象不多,所以很快。

2、concurrent-mark 并发标记阶段
由第一阶段标记过的对象出发,所有可达的对象都在本阶段标记。
3、concurrent-preclean 并发预清理阶段
也是一个并发执行的阶段。在本阶段,会查找前一阶段执行过程中,从新生代晋升或新分配或被更新的对象。通过并发地重新扫描这些对象,预清理阶段可以减少下一个stop-the-world 重新标记阶段的工作量。
4、concurrent-abortable-preclean 并发可中止的预清理阶段。这个阶段其实跟上一个阶段做的东西一样,也是为了减少下一个STW重新标记阶段的工作量。增加这一阶段是为了让我们可以控制这个阶段的结束时机,比如扫描多长时间(默认5秒)或者Eden区使用占比达到期望比例(默认50%)就结束本阶段。
5、remark 重标记阶段(CMS的第二个STW阶段)
暂停所有用户线程,从GC Root开始重新扫描整堆,标记存活的对象。需要注意的是,虽然CMS只回收老年代的垃圾对象,但是这个阶段依然需要扫描新生代,因为很多GC Root都在新生代,而这些GC Root指向的对象又在老年代,这称为“跨代引用”。
6、concurrent-sweep ,并发清理

垃圾回收机制(GC) - 图6
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:设定CMS的线程数量

垃圾回收器(分区模型)

  • G1:一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
  • ZGC:JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。
  • Shenandoah:由 Red Hat 的一个团队负责开发,与 G1 类似,基于 Region 设计的垃圾收集器,但不需要 Remember Set 或者 Card Table 来记录跨 Region 引用,停顿时间和堆的大小没有任何关系。停顿时间与 ZGC 接近,下图为与 CMS 和 G1 等收集器的 benchmark。

    4.常用[重点] G1收集器 -XX:+UseG1GC

    JDK9的默认垃圾收集器

    如果你的堆内存大于4G的话,那么G1会是要考虑使用的收集器。它是为了更好支持大于4G堆内存在JDK 7 u4引入的。
    G1收集器把堆分成多个区域,大小从1MB到32MB,并使用多个后台线程来扫描这些区域,优先会扫描最多垃圾的区域,这就是它名称的由来,垃圾优先Garbage First。

如果在后台线程完成扫描之前堆空间耗光的话,才会进行STW收集。它另外一个优点是它在处理的同时会整理压缩堆空间,相比CMS只会在完全STW收集的时候才会这么做。

使用过大的堆内存在过去几年是存在争议的,很多开发者从单个JVM分解成使用多个JVM的微服务(micro-service)和基于组件的架构。其他一些因素像分离程序组件、简化部署和避免重新加载类到内存的考虑也促进了这样的分离。

除了这些因素,最大的因素当然是避免在STW收集时JVM用户线程停滞时间过长,如果你使用了很大的堆内存的话就可能出现这种情况。另外,像Docker那样的容器技术让你可以在一台物理机器上轻松部署多个应用也加速了这种趋势。

ZGC

https://blog.csdn.net/qq_42709262/article/details/85092208

重要!!! 回收器搭配

-XX:+UseParNewGC
Parallel是并行的意思,ParNew收集器是Serial收集器的多线程版本,使用这个参数后会在新生代进行并行回收,老年代仍旧使用串行回收。新生代S区任然使用复制算法。操作系统是多核CPU上效果明显,单核CPU建议使用串行回收器。打印GC详情时ParNew标识着使用了ParNewGC回收器。默认关闭。

  1. [GC[ParNew: 78656K->8703K(78656K), 0.0180555 secs] 81048K->17429K(253440K), 0.0187828 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

-XX:+UseParallelGC
代表新生代使用Parallel收集器,老年代使用串行收集器。Parallel Scavenge收集器在各个方面都很类似ParNew收集器,它的目的是达到一个可以控制的吞吐量。吞吐量为CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机运行100分钟,垃圾收集花费1分钟,那吞吐量就99%。Server模式默认开启,其他模式默认关闭。
Parallel Scavenge提供了两个参数用于控制吞吐量,-XX:MaxGCPauseMillis参数用于设置最大停顿时间,它的参数运行值是一个大于0的毫秒数,收集器将尽力保证垃圾回收时间不超过设定值,系统运行的需要回收的垃圾总量是固定的,缩短停顿时间的同时会增大回收频度。-XX:GCTimeRatio参数用户控制垃圾回收时间占比,它运行的参数值是0-100的整数,如果参数设置为19,代表最大GC时间占总时间的5%(1/(1+19))。
Parallel收集器还提供了自适应的调节策略-XX:UseAdaptiveSizePolicy,即JVM会根据实际运行情况动态调整新生代大小、新生代和s区比例、晋升老年代对象大小等细节参数。
-XX:+UseParallelOldGC
新生代和老年代都使用并行收集器。打印出的GC会带PSYoungGen、ParOldGen关键字。

[Full GC [PSYoungGen: 4032K->0K(145920K)] [ParOldGen: 164500K->138362K(246272K)] 168532K->138362K(392192K) [PSPermGen: 67896K->67879K(136192K)], 1.006

-XX:+ UseCMSCompactAtFullCollection
Full GC后,进行一次整理,整理过程是独占的,会引起停顿时间变长。仅在使用CMS收集器时生效。
-XX:ParallelCMSThreads
设置并行GC时进行内存回收的线程数量
-XX:PreternureSizeThreshold
直接晋升老年代的对象大小,设置了这个参数后,大于这个参数的对象直接在老年代进行分配。
-XX:MaxTenuringThreshold
晋升老年代的对象年龄,对象在每一次Minor GC后年龄增加一岁,超过这个值后进入到老年代。默认值为15。
-XX:NativeMemoryTracking=detail
使用命令jcmd pid VM.native_memory detail,配合查看JVM相关情况

方法区中的垃圾回收:

(1) 常量池中一些常量、符号引用没有被引用,则会被清理出常量池
(2) 无用的类:被判定为无用的类,会被清理出方法区。