zoukankan      html  css  js  c++  java
  • 深入理解java虚拟机笔记Chapter3-垃圾收集器

    垃圾收集器

    垃圾收集(Garbage Collection,GC),它的任务是解决以下 3 件问题:

    • 哪些内存需要回收?
    • 什么时候回收?
    • 如何回收?

    本节补充知识:

    s:Survivor区

    • 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。

    • 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。

    • 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。

    设置Survivor区的意义在哪里?

    如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。

    为什么要设置两个Survivor区

    设置两个Survivor区最大的好处就是解决了碎片化。详见博客

    串行

    并行

    并发

    判断对象生死

    对象已死?

    堆中存放着Java世界中几乎所有的对象,垃圾收集器在对堆进行回收前,第一件事就是要确定哪些对象还“存活着”,哪些已经“死去”(即不可能再被任何途径使用的对象)。

    引用计数算法

    • 算法描述:
      • 给对象添加一个引用计数器;
      • 每有一个地方引用它,计数器加 1;
      • 引用失效时,计数器减 1;
      • 计数器值为 0 的对象不再可用。
    • 缺点:很难解决循环引用的问题。即 objA.instance = objB; objB.instance = objA;,objA 和 objB 都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为 0,将永远不能被判为不可用。

    实际上,Java并没有采用引用计数算法,因为它很难解决对象之间的相互循环引用的问题。

    可达性分析算法(主流)

    • 算法描述:
      • 从 "GC Root" 对象作为起点开始向下搜索,走过的路径称为引用链(Reference Chain);
      • 从 "GC Root" 开始,不可达的对象被判为不可用。

    如下图所示,object5,6,7与GC Roots是不可达的,故而判定为可回收的对象。

    • Java 中可作为 “GC Root” 的对象:
      • 栈中(本地变量表中的reference)
        • 虚拟机栈中,栈帧中的本地变量表引用的对象;
        • 本地方法栈中,JNI 引用的对象(native方法);
      • 方法区中
        • 类的静态属性引用的对象;
        • 常量引用的对象;

    即便如此,一个对象也不是一旦被判为不可达,就立即死去的,宣告一个的死亡需要经过两次标记过程。(详见本章的宣告对象死亡的两次标记过程)

    四种引用类型

    JDK 1.2 后,Java 中才有了后 3 种引用的实现。

    • 强引用: 像Object obj = new Object()这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
    • 软引用: 用来引用还存在但非必须的对象。对于软引用对象,在 OOM 前,虚拟机会把这些对象列入回收范围中进行第二次回收,如果这次回收后,内存还是不够用,就 OOM。实现类:SoftReference。
    • 弱引用: 被弱引用引用的对象只能生存到下一次垃圾收集前,一旦发生垃圾收集,被弱引用所引用的对象就会被清掉。实现类:WeakReference。
    • 虚引用: 幽灵引用,对对象没有半毛钱影响,甚至不能用来取得一个对象的实例。它唯一的用途就是:当被一个虚引用引用的对象被回收时,系统会收到这个对象被回收了的通知。实现类:PhantomReference。

    宣告对象死亡的两次标记过程

    在根搜索算法不可达的对象,也并非是“非死不可”的,它们暂时处于“死缓”状态,要真正宣告对象的死亡,至少要经历两次标记:

    • 当发现对象不可达后,该对象被第一次标记,并进行是否有必要执行 finalize() 方法的判断;
      • 不需要执行:对象没有覆盖 finalize() 方法,或者 finalize() 方法已被执行过(finalize() 只被执行一次);
      • 需要执行:将该对象放置在一个队列中,稍后由一个虚拟机自动创建的低优先级线程执行。
    • finalize() 方法是对象逃脱死亡的最后一次机会,不过虚拟机不保证等待 finalize() 方法执行结束,也就是说,虚拟机只触发 finalize() 方法的执行,如果这个方法要执行超久,那么虚拟机并不等待它执行结束,所以最好不要用这个方法。
    • finalize() 方法能做的,try-finally 都能做,所以忘了这个方法吧!

    方法区的回收 永久代的 GC 主要回收:废弃常量无用的类

    • 废弃常量:例如一个字符串 "abc",当没有任何引用指向 "abc" 时,它就是废弃常量了。
    • 无用的类:同时满足以下 3 个条件的类。
      • 该类的所有实例已被回收,Java 堆中不存在该类的任何实例;
      • 加载该类的 Classloader 已被回收;
      • 该类的 Class 对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法。

    垃圾收集算法

    基础:标记 - 清除算法

    • 算法描述:
      • 先标记出所有需要回收的对象(图中深色区域);
      • 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……)。
    • 不足:
      • 效率问题:标记和清理两个过程的效率都不高。
      • 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC。

    解决效率问题:复制算法

    • 算法描述:
      • 将可用内存分为大小相等的两块,每次只使用其中一块;
      • 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
    • 不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。
    • 节省内存的方法:
      • 新生代中的对象 98% 都是朝生夕死的,所以不需要按照 1:1 的比例对内存进行划分;
      • 把内存划分为:
        • 1 块比较大的 Eden 区;
        • 2 块较小的 Survivor 区;
      • 每次使用 Eden 区和 1 块 Survivor 区;
      • 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空;
      • JVM 参数设置:-XX:SurvivorRatio=8表示 Eden 区大小 / 1 块 Survivor 区大小 = 8

    解决空间碎片问题:标记 - 整理算法

    • 算法描述:
      • 标记方法与 “标记 - 清除算法” 一样;
      • 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
    • 不足: 存在效率问题,适合老年代。

    进化:分代收集算法

    • 新生代: GC 过后只有少量对象存活 —— 复制算法
    • 老年代: GC 过后对象存活率高 —— 标记 - 整理算法

    7 个垃圾收集器

    垃圾收集器就是内存回收操作的具体实现,HotSpot 里足足有 7 种,为啥要弄这么多,因为它们各有各的适用场景。有的属于新生代收集器,有的属于老年代收集器,所以一般是搭配使用的(除了万能的 G1)。关于它们的简单介绍以及分类请见下图。

    Serial 收集器

    • 单线程收集器
    • 在进行垃圾收集时,必需暂停其他所有的工作线程(打扫卫生时,必需要求房间里停止工作产生垃圾)
    • 简单而高效,专心做垃圾收集
    • 虚拟机运行在Client模式下的默认新生代收集器

    ParNew 收集器

    • ParNew收集器其实就是Serial收集器的多线程版本,是运行在Server模式下的虚拟机中首选的新生代收集器。

    • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程处于等待状态。

    • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替运行),用户程序继续运行,而垃圾收集线程运行在另一个CPU上。

    Parallel Scavenge

    Parallel Scavenge收集器是一个使用复制算法的并行的多线程收集器,它关注于提高吞吐量(Throughput,CPU用于运行用户代码的时间和CPU消耗时间的比值)。另外,自适应调节策略也是Parallel Scavenge和ParNew收集器的区别.

    Serial Old 收集器

    是Serial收集器的老年版本。

    Parallel Old 收集器

    是Parallel Scavenge 收集器的老年版本。


    介绍下上述垃圾收集器搭配使用的方法

    Serial / ParNew 搭配 Serial Old 收集器

    • Serial 收集器是虚拟机在 Client 模式下的默认新生代收集器,它的优势是简单高效,在单 CPU 模式下很牛。

    • ParNew 收集器就是 Serial 收集器的多线程版本,虽然除此之外没什么创新之处,但它却是许多运行在 Server 模式下的虚拟机中的首选新生代收集器,因为除了 Serial 收集器外,只有它能和 CMS 收集器搭配使用。

    Parallel 搭配 Parallel Scavenge 收集器

    首先,这两个收集器肯定是要搭配使用的,不仅仅如此,它们的关注点与其他收集器不同,其他收集器关注于尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目的是达到一个可控的吞吐量。

    吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )
    

    因此,Parallel Scavenge 收集器不管是新生代还是老年代都是多个线程同时进行垃圾收集,十分适合于应用在注重吞吐量以及 CPU 资源敏感的场合。

    可调节的虚拟机参数:

    • -XX:MaxGCPauseMillis:最大 GC 停顿的秒数;
    • -XX:GCTimeRatio:吞吐量大小,一个 0 ~ 100 的数,最大 GC 时间占总时间的比率 = 1 / (GCTimeRatio + 1)
    • -XX:+UseAdaptiveSizePolicy:一个开关参数,打开后就无需手工指定 -Xmn-XX:SurvivorRatio 等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自行调整。

    CMS 收集器

    CMS(Concurrent Mask Sweep)收集器是一个以获取最短回收停顿时间为目标的收集器。因此,此收集器特别适合现代的互联网或 B/S 架构的服务端上。

    CMS 收集器是基于“标记-清除”算法实现的,整个过程分为4个步骤:

    1. 初始标记
    2. 并发标记
    3. 重新标记
    4. 并发清除

    优点是:并发收集、低停顿
    缺点是:对CPU资源非常敏感、无法处理浮动垃圾、收集结束后会产生大量的空间碎片以致于在给大对象分配空间时带来麻烦

    • 参数设置:
      • -XX:+UseCMSCompactAtFullCollection:在 CMS 要进行 Full GC 时进行内存碎片整理(默认开启)
      • -XX:CMSFullGCsBeforeCompaction:在多少次 Full GC 后进行一次空间整理(默认是 0,即每一次 Full GC 后都进行一次空间整理)

    G1收集器

    G1(Garbage First)收集器是当前收集器技术发展的最新成果,相对于上文的 CMS 收集器有两个显著改进:

    • 基于“标记-整理”算法,也就是说它不会产生空间碎片
    • 非常精确地控制停顿

    G1 收集器可以实现在基本不牺牲吞吐量的情况下完成低停顿的回收,它将整个Java堆划分为多个大小固定的独立区域(Region),并跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这也是Garbage First名称的由来)。总而言之,区域划分和有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

    垃圾收集器参数总结

    -XX:+UseSerialGC:在新生代和老年代使用串行收集器
    -XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
    -XX:NewRatio:新生代和老年代的比
    -XX:+UseParNewGC:在新生代使用并行收集器
    -XX:+UseParallelGC :新生代使用并行回收收集器
    -XX:+UseParallelOldGC:老年代使用并行回收收集器
    -XX:ParallelGCThreads:设置用于垃圾回收的线程数
    -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
    -XX:ParallelCMSThreads:设定CMS的线程数量
    -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
    -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
    -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
    -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
    -XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
    -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
    

    GC日志解读

  • 相关阅读:
    Nginx 基本命令
    Nginx配置详细
    MySQL 函数大全
    X-Frame-Options 配置
    Visual Studio提示“无法启动IIS Express Web服务器”的解决方法
    idea java 非web程序打包
    mysql 存储过程
    webstorm 重置所有设置
    vue input 赋值无效
    MySQL 性能优化神器 Explain 使用分析
  • 原文地址:https://www.cnblogs.com/kylinxxx/p/13773399.html
Copyright © 2011-2022 走看看