【JVM】垃圾回收的基础知识

什么是垃圾?

没有任何引用指向的对象,就是垃圾

如何找到垃圾?(2 种方法)

过程:先找到正在使用的对象,然后把没有正在使用的对象进行回收

1.引用数-Reference-Count


被引用数为 0 的即为垃圾,在 Java 领域,至少主流的 Java 虚拟机里面都没有选用引用计数算法来管理内存。原因是该算法不能回收循环引用,有缺陷,怎么办呢?根可达算法可以弥补这个缺陷(Root Searching)

2.根可达算法-Root Searching(这个必须要牢牢记住)

  • 如何理解根可达?
    算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的

  • GC Roots(根对象)都有哪些东西?

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  2. 类的静态成员变量的引用:T.class 对静态变量初始化能够访问到的对象是根对象
  3. 常量池:一个 class 用到的其他的 class 对象叫做根对象
  4. JNI 指针:本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
  • 什么是根对象?
    一个线程启动时需要的对象就是根对象

引用数和根可达算法都提到了“引用”,Java中有哪些引用类型?

  • 强引用(不被回收)
    当内存空间不足,系统撑不住了,JVM 就会抛出OutOfMemoryError 错误。即使程序会异常终止,这种对象也不会被回收。这种引用属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉

  • 软引用(内存够不回收,不够再回收)
    在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。

  • 弱引用(只要发生GC,就会被回收)
    当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期

  • 虚引用
    这是一种形同虚设的引用,在现实场景中用的不是很多

如何清除垃圾?(三种算法,必须背过)

  • 标记清除-Mark-Sweep

    先标记,再清除,存活对象比较多的时候,清除的效率比较高,但是需要扫描两遍(如何理解 2 遍?第一次扫描先找到那些有用的,第二次扫描再找到那些没用的并清除),效率偏低,很容易产生碎片

  • 拷贝-Copying

    拷贝也称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存 活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复 制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有 空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷 也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费太多了一 点,但是效率是最高的
    优点:适用于存活对象较少的情况,而且只扫描一次,效率比较高,且没有碎片
    缺点:空间浪费,移动复制对象,需要调整对象引用

  • 标记压缩为紧凑-Mark-Compact

    清理垃圾的过程中,把存活的对象全部扔到前面的位置,然后大块的内存就出来了
    优点:不会产生碎片,不会产生内存减半的问题
    缺点:也需要扫描两次,第一次标记有用对象,第二次移动对象,如果移动的过程是多线程的效率就会低很多

堆内存逻辑分区

研究表明,大部分对象,可以分为 2 类

  • 大部分对象的生命周期都很短;

  • 其他对象则很可能会存活很长时间

    大部分死的快,其他的活的长。这个假设称之为弱代假设

    现在的垃圾回收器,都会在物理上或者逻辑上,把这两类对象进行区分。我们把死的快的对象所占的区域,叫作年轻代(Young generation)。把其他活的长的对象所占的区域,叫作老年代(Old generation)

年轻代

  • 年轻代采用的算法
    采用复制算法(Copying)。因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。

    复制算法会造成一定的空间浪费,所以年轻代中间也会分很多区域:年轻代分为:一个伊甸园空间(Eden ),两个幸存者空间(Survivor )

  • 年轻代的 GC 过程
    一个对象产生之后首先在栈上分配,如果栈上空间不够,会进入伊甸区(Eden),当年轻代中的 Eden 区分配满的时候,就会触发年轻代的 GC(Minor GC)。具体过程如下:
    1. 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区(以下简称 from);
    2. Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理。存活的对象会被复制到 to 区;接下来,只需要清空 from 区就可以了。
    所以在这个过程中,总会有一个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。这个比例,是由参数-XX:SurvivorRatio进行配置的(默认为 8)。

  • 一个对象的分配逻辑

    1.栈上分配
    栈上分配对象比在堆上分配要快很多
    2.TLAB 上分配
    TLAB 的全称是 Thread Local Allocation Buffer,JVM 默认给每个线程在 Eden 区中开辟一个 buffer 区域,默认占用 1%Eden 的空间,用来加速对象分配,这个道理和 Java 语言中的 ThreadLocal 类似,避免了对公共区的操作,以及一些锁竞争。
    3.Eden 分配

老年代

  • 老年代垃圾回收算法
    老年代一般使用“标记-清除”、“标记-整理”算法,因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,还不如采取就地收集的方式。
  • 对象是怎么进入老年代的?
  1. 提升(Promotion):年龄达到阈值则进入老年代
    如果对象够老,会通过“提升”进入老年代。关于对象老不老,是通过它的年龄(age)来判断的。每当发生一次 Minor GC,存活下来的对象年龄都会加 1。直到达到一定的阈值,就会把这些“老顽固”给提升到老年代。这些对象如果变的不可达,直到老年代发生 GC 的时候,才会被清理掉。这个阈值,可以通过参数‐XX:+MaxTenuringThreshold 进行配置,最大值是 15,因为它是用 4bit 存储的(所以网络上那些要把这个值调的很大的文章,是没有什么根据的)。

  2. 分配担保:Survivor 空间不够,直接分配到老年代空间
    每次存活的对象,都会放入其中一个幸存区,这个区域默认的比例是 10%。但是我们无法保证每次存活的对象都小于 10%,当 Survivor 空间不够,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配

  3. 大对象直接在老年代分配
    超出某个大小的对象将直接在老年代分配。这个值是通过参数-XX:PretenureSizeThreshold进行配置的。默认为 0,意思是全部首选 Eden 区进行分配。

  4. 动态对象年龄判定
    有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。比如,如果幸存区中相同年龄对象大小的和,大于幸存区空间大小的一半,大于或等于 age 的对象将会直接进入老年代。

JVM 内存分代模型

除了 ZGC,Epsilon , Shenandiah 之外,都是使用逻辑分代模型
G1 是逻辑分代,物理不分代,除此之外不仅逻辑分代,而且物理分代
YGC:年轻代空间耗尽时触发
FullGC:在老年代无法继续分配空间的时候触发,新生代,老年代同时进行回收

逃逸分析

https://www.cnblogs.com/javastack/p/12923778.html
逃逸:某个变量只在某个方法内部有效叫做无逃逸,如果不止在某个方法内有效,则是逃逸的。类的成员变量是逃逸的,方法变量则是无逃逸的

  • 查看 JVM 参数
java -XX:+PrintCommandLineFlags -version

垃圾收集器跟内存大小的关系

  1. Serial 几十兆
  2. PS 上百兆 - 几个G
  3. CMS - 4~6G以下的堆内存
  4. G1 - 6G以上的
  5. ZGC - 4T - 16T(JDK13)

常见垃圾回收器组合参数设定

-XX:+UseSerialGC 年轻代和老年代都用串行收集器
-XX:+UseParNewGC 年轻代使用ParNew,老年代使用 Serial Old
-XX:+UseParallelGC 年轻代使用Paraller Scavenge,老年代使用Serial Old
-XX:+UseParallelOldGC 新生代Paraller Scavenge,老年代使用Paraller Old
-XX:+UseConcMarkSweepGC,表示年轻代使用ParNew,老年代的用CMS + Serial Old
-XX:+UseG1GC 使用G1垃圾回收器
-XX:+UseZGC 使用ZGC垃圾回收器

STW

如果在垃圾回收的时候(不管是标记还是整理复制),又有新的对象进入怎么办?
为了保证程序不会乱套,最好的办法就是暂停用户的一切线程。也就是在这段时间,你是不能 new 对象的,只能等待。表现在 JVM 上就是短暂的卡顿,什么都干不了。这个头疼的现象,就叫作 Stop the world。简称 STW。
标记阶段,大多数是要 STW 的。如果不暂停用户进程,在标记对象的时候,有可能有其他用户线程会产生一些新的对象和引用,造成混乱。
现在的垃圾回收器,都会尽量去减少这个过程。但即使是最先进的 ZGC,也会有短暂的 STW过程。我们要做的就是在现有基础设施上,尽量减少 GC 停顿。

1. Serial(回收时会停顿,现在用的很少)

单线程清理垃圾,(STW)Stop The World
其中有一个 safe point 的概念需要注意:线程并不是立马就停下来,而是找一个安全点停止
单机 CPU 效率最高,内存比较小的时候可以接受这种垃圾回收器,内存越大,垃圾回收的时间越长,因此这种垃圾回收器在 server 端使用的越来越少。通常使用在客户端上

2.Serial Old 组合

单线程在老年代回收垃圾

3.ParallelScavenge + Parallel Old 组合(简称 PS+PO 还有很多公司在使用,如果不设置,就是这么一个组合)

PS 使用复制算法清除垃圾
PO 使用标记整理算法清除垃圾
多线程清理垃圾

4.ParNew(Parallel New)+ CMS 组合

ParNew 是 ParallelScavenge 的一个增强变种,为了和 CMS 搭配使用
(STW)Stop The World

原文地址:https://www.cnblogs.com/zhangyibing/p/13786446.html