垃圾回收

52 阅读10分钟

一.如何知道对象是一个垃圾?

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

  • 可达性分析(非GCRoot引用的对象)

    1. 通过判断对象的引用链是否可达来决定对象是否可以被回收
    2. 通过一系列名为GCRoot的对象为起始点,从这些节点开始向下搜索,搜索所走过的路径就被称为引用链
    3. 当一个对象从GCRoot没有任何引用链相连,即从GCRoot到这个对象是不可达的,在这个时候就证明了这个对象是不可用的,可以进行回收
  • 引用计数法(判断对象的引用数量来决定对象是否可以被回收)

    1. 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
    2. 任何引用计数为0的对象实例可以被当做垃圾收集
      优点:执行效率高,程序执行受影响较小
      缺点:无法检测出循环引用的情况,导致内存泄漏

二.可以作为GCRoot的对象有哪些?

  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中JNI(native方法)的引用对象
  • 活跃线程的引用对象

三.GC算法

  • 1.标记清除(有碎片)
  • 2.拷贝算法(浪费空间,一般用于MinorGC,对于新生代的)
  • 3.标记整理(一边做标记一边做整理,但是效率低)

标记清除标记整理算法一般都是针对老年代进行回收的算法

四.垃圾回收器

需要了解的相关概念:

  • 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • Stop-the-world(STW)
    JVM由于要执行GC而停止了应用程序的执行
    任何一种GC算法中都会发生
    多数GC优化通过减少Stop-the-world发生的时间来提升程序性能
  • Safepoint(安全点)
    分析可达性的过程中引用关系不会发生变化的点
    产生Safepoint的地方:方法调用、循环跳转、异常跳转等
    安全点的数量需要适中。太少会让GC等待太长的时间,太多会增加程序运行的负荷
  • JVM的运行模式
    Server:启动较慢,但是启动成功之后server程序的运行效率要比Client快。采用的是重量级的虚拟机,对程序采用了更多的优化。
    Client:启动较快,采用的是轻量级的虚拟机
    通过java -version命令进行查看

1.分代模型

对于新生代:

  • Serial(最早的垃圾回收器,现在很少用,单线程)
    可以通过-XX:+UseSerialGC,使得年轻代使用该垃圾回收期进行回收
    在jdk1.3之前,是Java年轻代垃圾回收器的唯一选择
    单线程通过复制算法进行收集,在进行垃圾收集时,必须暂停所有的工作线程
    简单高效,是Client模式下默认的年轻代收集器
  • ParNew(多线程处理,工作在年轻代,专门和CMS做配合的)
    可以通过-XX:+UseParNewGC,使得年轻代使用该垃圾回收期进行回收
    多线程收集,其余的行为、特点和Serial收集器一样
    是Server模式下首选的年轻代回收器
    也是使用复制算法
    单核执行效率不如Serial,在多核下执行才有优势
  • Parallel Scavenge(多线程处理,工作在年轻代)
    可以通过-XX:+UseParallelGC,使得年轻代使用该垃圾回收期进行回收
    使用复制算法
    比起关注用户线程停顿时间,更关注系统的吞吐量
    在多核执行下才有优势,Server模式下默认的年轻代收集器
    自适应调节策略:-XX:+UseAdaptiveSizePolicy,会把内置管理的调优任务交给虚拟机去完成

对于老年代:

  • Serial Old(单线程)
    可以通过-XX:+UseSerialOldGC,使得老年代使用该垃圾回收期进行回收
    单线程通过标记-整理算法进行收集,在进行垃圾收集时,必须暂停所有的工作线程
    简单高效,是Client模式下默认的老年代收集器
  • Parallel Old(多线程处理)
    可以通过-XX:+UseParallelOldGC,使得老年代使用该垃圾回收期进行回收
    多线程使用标记-整理算法进行收集,JDK6之后开始提供的,吞吐量优先
  • CMS
    标记-清除算法
    垃圾回收线程几乎能与用户线程做到同时工作,只是尽可能缩短了停顿时间
    JDK5提出的第一款针对于老年代GC与工作线程并发执行的收集器

2.分区模型

  • G1(1.8可用)
  • ZGC
  • Shenandoah
  • Epsilon(无用)

3.关于JDK1.8

  • 1.8默认的垃圾回收器:PS+PO
  • 1.8推荐用G1

4.CMS垃圾回收器

  • 中文:concurrent mark sweep
  • 沿着路线在前进:内存越来越大,卡顿时间越来越短
  • 垃圾回收线程和工作线程可以一起并行工作。
  • 大致六个阶段,主要有四个阶段:1.初始标记;2.并发标记;3.重新标记;4.并发清理
    1.初始标记:STW的时间很短,降低STW时间;G1垃圾回收器在STW的时间更短
    2.并发标记:会跟工作线程发生混乱,会发生错标的问题
    3.并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
    4.重新标记:修正那些错标的对象,remark阶段,必须从头扫描一遍,暂停虚拟机,扫描堆中的剩余对象
    5.并发清理:也是并发执行,发生错标问题,但是没关系,下次循环可以解决
    6.并发重置:重置CMS收集器的数据结构,等待下一次垃圾回收

对于三色标记算法产生的漏标问题: CMS的解决方案为:incremental update

5.G1垃圾回收器
  • 既用于新生代,也可以用于老年代的垃圾回收器
  • 使用复制+标记-整理算法进行回收
  • 设计的目的是为了替换掉JDK5中发布的CMS垃圾回收期
  • 并发+并行,使用多个CPU来缩短STW的停顿时间,与用户线程并发执行
  • 分代收集,独立管理整个堆
  • 空间整合,基于标记整理算法,解决了内存碎片的问题
  • 可预测的停顿,能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为n毫秒的时间片段内消耗在垃圾收集上的时间不得超过n毫秒,这个n是可以设置的。
  • 用到了读屏障和写屏障
  • 对于三色标记算法产生的漏标问题,对应的算法为SATB
  • 将整个Java堆内存划分成多个大小相等的Region区
  • 年轻代和老年代不再物理隔离
5.触发fullGC的情况
  • 老年代空间不足
  • 永久代空间不足(jdk8之后没有了永久代,该条件不成立,取而代之的是元空间,目的是为了降低fullGC的频率,减少GC的负担,提升效率),只针对1.7
  • CMS GC时出现promotionfailed,concurrent mode failure
  • MinorGC晋升到老年代平均大小大于老年代的剩余空间
  • 调用System.gc(),不会立即回收,而是通知jvm进行回收,回收时机需要等底层进行
  • 使用RMI来进行RPC或管理的JDK的应用,每小时执行一次FullGC

五.内存模型

1.分代

1.1 新生代
  • 新生代就是刚刚new出来的对象

  • 新生代可以理解为大多数一次垃圾回收就能回收掉的对象

  • 在新生代里面用拷贝算法效率极高

  • 年轻代分为三个区域:Eden、2个survivor(幸存者)。比例为:8:1:1

  • 不能回收的对象在两个survivor区中来回存活,直到达到-XX:MaxTenuringThreshold设置的最大年龄则进入老年代,默认为16

  • CMS的默认的一个对象在新生代的最大年龄为16

  • 新生代空间耗尽触发MinorGC/YGC

    1.2 老年代
  • 老年代就是经过了好多次垃圾回收之后老是回收不了的对象

  • 垃圾回收不容易回收掉的对象

  • 在老年代一般用标记清除或者标记整理

  • 在老年代无法继续分配空间时触发MajorGC/FullGC,新生代老年代同时进行回收

  • 大对象直接放在老年代(有参数可以确定)

六.对象回收步骤

  • 当new一个对象的时候,判断是否可以不在堆上进行分配(可以在栈上进行分配,只要栈pop之后,对象就可以直接被回收,不用被GC所回收),但是这个栈上的对象有两个前提:逃逸分析(一个方法中的对象不能被另一个方法所引用)和标量替换(基础数据类型)
  • 如果对象是个大对象,直接放到老年代
  • 如果不是大对象,不管是不是TLAB(线程本地分配缓冲区),都会进入Eden区,然后进行GC清除,能清除的直接清除。清除不了的进入survivor1区域,年龄够了进入老年代,年龄不够进入survivor2,循环往复。

七.GC常用参数

  • -Xmn:年轻代

  • -Xms:最小堆内存

  • -Xmx:最大堆内存

  • -Xss:栈空间

    关于G1的参数

  • -XX:+UseG1GC

  • -XX:MaxGCPauseMillis(建议值,G1会尝试调整young区的块数来达到这个值)

  • -XX:GCPauseIntervalMillis(GC的间隔时间)