垃圾收集器

垃圾收集(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日志解读

深入理解java虚拟机笔记Chapter3-垃圾收集器的更多相关文章

  1. 《深入理解 Java 虚拟机》学习 -- 垃圾收集器

    <深入理解 Java 虚拟机>学习 -- 垃圾收集器 1. Serial 收集器(新生代) 含义: 单线程收集器. 缺点: 进行垃圾收集时,必须暂停其他所有的工作线程. 优点: 简单而高效 ...

  2. 深入理解Java虚拟机(四)——HotSpot垃圾收集器详解

    垃圾收集器 新生代收集器 1.Serial收集器 特点: 单线程工作,收集的时候就会停止其他所有工作线程,用户不可知不可控,会使得用户界面出现停顿. 简单高效,是所有收集器中额外内存消耗最少的. 没有 ...

  3. Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一

    Java内存区域 对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出.内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有 ...

  4. 垃圾收集器与内存分配策略——深入理解Java虚拟机 笔记二

    在本篇中,作者大量篇幅介绍了当时较为流行的垃圾回收器,但现在Java 14都发布了,垃圾收集器也是有了很大的进步和发展,因此在此就不再对垃圾收集器进行详细的研究.但其基本的算法思想还是值得我们参考学习 ...

  5. 深入理解Java虚拟机笔记——垃圾收集器与内存分配策略

    目录 判断对象是否死亡 引用计数器算法 可达性分析算法 各种引用 回收方法区 垃圾收集算法 标记-清除算法 复制算法 标记-整理算法 分代收集算法 HotSpot算法实现 枚举根节点 GC停顿(Sto ...

  6. 深入理解Java虚拟机笔记

    1. Java虚拟机所管理的内存 2. 对象创建过程 3. GC收集 4. HotSpot算法的实现 5. 垃圾收集器 6. 对象分配内存与回收细节 7. 类文件结构 8. 虚拟机类加载机制 9.类加 ...

  7. 深入理解java虚拟机笔记Chapter12

    (本节笔记的线程收录在线程/并发相关的笔记中,未在此处提及) Java内存模型 Java 内存模型主要由以下三部分构成:1 个主内存.n 个线程.n 个工作内存(与线程一一对应) 主内存与工作内存 J ...

  8. 深入理解Java虚拟机03--垃圾收集器与内存分配策略

    一.概述  哪些内存需要回收? 什么时候回收? 如何回收? 二.对象已死吗  1.引用计数算法  定义:给对象添加一个引用计数器,当增加一个引用时,加1,当一个引用时,减1; 缺陷:当对象之间互相循环 ...

  9. 深入理解java虚拟机:笔记

    1.运行时数据区域 1.程序计数器 当前线程执行字节码的行号指示器,字节码解释器工作通过改变这个计数器的值来选取下一条需要执行的字节码指令,每一个线程拥有独立的程序计数器,线程私有的内存 2.虚拟机栈 ...

随机推荐

  1. hdu4038贪心(最快上升倍率,好题)

    题意:       给你n个数,然后有两种操作 1.给其中的一个数+1,2.在序列里面增加一个1,然后给你一个m,表示进行了m次操作,最后问你操作之后所有数乘积最大是多少? 思路:      徒弟给我 ...

  2. Windows核心编程 第九章 线程与内核对象的同步(上)

    第9章 线程与内核对象的同步 上一章介绍了如何使用允许线程保留在用户方式中的机制来实现线程同步的方法.用户方式同步的优点是它的同步速度非常快.如果强调线程的运行速度,那么首先应该确定用户方式的线程同步 ...

  3. 【JavaScript】Leetcode每日一题-最大整除子集

    [JavaScript]Leetcode每日一题-最大整除子集 [题目描述] 给你一个由 无重复 正整数组成的集合 nums ,请你找出并返回其中最大的整除子集 answer ,子集中每一元素对(an ...

  4. window 10 删除文件夹需要管理员权限

    如果设了当前的账号有权限删除了, 还是显示删除里管理员则需要改: 按Win+R组合键,输入gpedit.msc点击确定: 在窗口边依次打开计算机配置--Windows设置--安全设置--本地策略--安 ...

  5. promise用法解析

    Promise的理解 Promise是对异步操作的一种解决方案,一般情况下,如果有异步操作,就需要使用Promise对这个异步操作进行封装 使用Promise后可以使代码看起来更加优雅并且易于维护 使 ...

  6. Nmap浅析(2)——端口发现

    端口发现 ​ 每台网络设备最多有216(65536)个端口,端口的作用是实现"一机多用".操作系统分了65536个端口号,程序在发送的信息中加入端口号,操作系统在接收到信息后按照端 ...

  7. Django(15)外键和表关系

    外键删除操作 如果一个模型使用了外键.那么在对方那个模型被删掉后,该进行什么样的操作.可以通过on_delete来指定.可以指定的类型如下: CASCADE:级联操作.如果外键对应的那条数据被删除了, ...

  8. [Python] 地图API

    请求位置信息 https://restapi.amap.com/v3/place/text?keywords=北京大学&city=beijing&output=xml&offs ...

  9. [刷题] 1 Two Sum

    要求 给出一个整型数组nums 返回这个数组中两个数字的索引值i和j 使得nums[i]+nums[j]等于一个给定的target值 两个索引不能相等 实例 nums=[2,7,11,15], tar ...

  10. 什么是CPU缓存

    一.什么是CPU缓存 1. CPU缓存的来历 众所周知,CPU是计算机的大脑,它负责执行程序的指令,而内存负责存数据, 包括程序自身的数据.在很多年前,CPU的频率与内存总线的频率在同一层面上.内存的 ...