JVM面试(四)-垃圾回收、垃圾收集器、GC日志
垃圾回收、垃圾收集器、GC日志
- 什么是垃圾?(垃圾的概念)
- 什么是垃圾回收?(垃圾回收的概念)
- 为什么要垃圾回收?(垃圾回收的原因)
- 如何定义垃圾?
- 引用计数算法
- 什么是循环引用
- 可达性分析算法
- 哪些可以作为GC Roots?
- GC Roots的判断办法(如何判断是不是GC Roots)
- 如何查看GC Roots
- 关于finalize方法
- 阅读参考
- 小结
- 强引用、软引用、弱引用、虚引用
- 分代收集理论
- Minor GC、Major GC和Full GC
- 三种GC的区别
- 三种GC触发条件和解决办法(重要!!!)
- 阅读参考
- Stop the World-STW
- System.gc()
- 内存溢出和内存泄漏(重要)
- 阅读参考
- 垃圾回收算法
- 标记-清除算法(Mark-Sweep)
- 标记-复制算法(Copying)
- 标记-整理算法(Mark-Compact)
- 分代收集算法(Generational Collecting)
- 分区收集算法
- 增量收集算法(Incremental Collecting)
- 阅读参考
- GC性能指标
- 阅读参考
- 几个概念(下面的垃圾收集器中会遇到)
- 并行-Parallel 与 并发(重要!!!)
- 根节点 枚举(动词)
- 安全点
- 安全区域
- 阅读参考
- 垃圾收集器
- 垃圾收集器分类
- Serial垃圾收集器
- 特点
- 参数
- 优势
- 应用场景
- Serial Old(老年代)垃圾收集器
- 特点
- 参数
- Par(Parallel)New(New表示只能处理新生代)垃圾收集器
- 特点
- 参数
- 应用场景
- 为什么只有ParNew能与CMS收集器配合?
- Parallel Scavenge垃圾收集器(吞吐量优先收集器,新生代)
- 特点
- 参数
- 应用场景
- Parallel Old垃圾收集器
- 特点
- 参数
- CMS(Concurrent Mark Sweep)垃圾收集器(重要!!!)
- 特点
- 缺点(重要!!!)
- 参数
- 应用场景
- CMS回收过程
- G1(Garbage-First)垃圾收集器(重要!!!)
- 特点
- 参数
- 优势
- 应用场景
- G1回收过程
- G1和CMS比较-待完善(重要!!!)
- 阅读参考
- 垃圾收集器总结
- 新生代收集器/老年代收集器
- 吞吐量优先、停顿时间优先
- 串行、并行、并发(重要!!!)
- 回收算法
- JDK默认垃圾收集器(重要!!!)
- 表格
- 垃圾收集器参数总结
- 如何选择垃圾收集器(重要!!!)
- 回收方法区(重要!!!)
- 方法区相关参数
- GC日志
- GC日志相关参数
- GC日志内容
什么是垃圾?(垃圾的概念)
没有任何指针指向的对象 就是垃圾
什么是垃圾回收?(垃圾回收的概念)
垃圾回收(Garbage Collection,GC),顾名思义就是 释放 垃圾占用的空间,防止内存泄露
有效的 使用(动词) 可使用的内存,对 内存堆中 已经死亡的 或者 长时间没有使用的对象 进行 清除和回收
为什么要垃圾回收?(垃圾回收的原因)
- 垃圾需要及时被清理,如果一直不进行清理,这些垃圾对象所占用的空间和会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至导致内存溢出
- 垃圾回收 除了 释放没有用的对象,还可以 清除 内存里的记录碎片,碎片整理将堆内存已到堆的一端,以便JVM将整理出的内存分配给新的对象
- 随着应用程序越来越复杂,用户越来越多,没有GC就不能保证应用程序的正常进行
如何定义垃圾?
- 在进行垃圾回收之前,需要判断哪些对象是存活对象,哪些是死亡对象,只有被标记为死亡的对象才能够被回收
- 当一个对象 已经不再被 任何的存活对象 继续引用 的时候,就可以宣判为已经死亡
- 判断对象是否存活一般有两种方式:引用计数算法 和 可达性分析算法
引用计数算法
通过 在 对象头中 分配一个空间 来保存 该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收
优点:
- 实现简单、垃圾便于辨识
- 判定效率高,回收没有延迟
缺点:
- 需要 单独的字段 存储计数器,额外的存储空间开销
- 需要 更新 计数器,伴随着加法和减法操作,带来时间开销
- 无法处理循环引用的情况
什么是循环引用
- 定义2个对象
- 相互引用
- 置空各自的声明引用
此时由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,很难解决对象之间循环引用问题,也就永远无法通知GC收集器回收它们
可达性分析算法
通过一些 被称为 引用链(GC Roots)的对象 作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当 一个对象 到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的
优点:实现简单,执行高效,而且能够解决循环引用的问题
要使用可达性分析算法判断内存是否可回收,那么 分析工作 必须在 一个能保障一致性 的 快照中进行,为了保持一致性,GC的时候必须Stop The World(STW)
哪些可以作为GC Roots?
- 虚拟机栈(栈帧中的本地变量表)中 引用的对象
- 方法区中 类(的)静态属性(static变量) 引用的对象
- 方法区中 常量(比如字符串常量池) 引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 所有 被同步锁synchronized 持有的对象
- Java内部的引用(基本数据类型 对应的 Class对象、一些常驻的异常对象[NullPointException、OutOfMemroyError等]、系统类加载器)
- 反应Java虚拟机内部情况的JMXBean、JVMTI注册的回调、本地代码缓存等
GC Roots的判断办法(如何判断是不是GC Roots)
如果一个指针,指向了 堆内存里面的对象,但是 自己又不存放在堆里面(虚拟机栈、本地方法栈中 指向堆区对象 的 引用、方法区中 对 堆区 静态变量 以及 字符串常量 的 引用),那它就是一个Root
如何查看GC Roots
阅读参考:使用MAT和JProfiler查看GC Roots
关于finalize方法
在可达性分析算法中,不可达的对象,暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历过两次标记过程
如果 对象 在可达性分析后 发现 没有与GC Roots相连接的引用链,那它将会被 第一次标记 并且 进行一次筛选,筛选的条件是 此对象 是否有必要执行 finalize()方法
finalize()方法是Object类中定义的,允许在任何子类中被重写,用于 对象被回收时 进行资源释放
当对象没有覆盖finalize()方法,或者已经被调用过finalize()了,JVM将这两种情况都视为“没有必须要执行” finalize()方法
如果被判定为“有必要执行”finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后 由一条 由虚拟机自动建立的、低调度优先级 的 Finalizer线程 去执行 它们的finalize()方法
这里所说的“执行” 是指 虚拟机 会触发这个方法开始运行,但并不承诺一定会等待它运行结束
这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记
如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,譬如 把自己(this关键字)赋值给 某个类变量 或者 对象的成员变量,那在第二次标记时 它将被移出 “即将回收”的集合
如果对象这时候还没有逃脱,那基本上它就真的要被回收了
重点!!!:任何一个对象 的 finalize方法 都只会被系统 自动调用一次
阅读参考
对象的finalization机制、finalize方法理解(认真阅读)
小结
- 基于可达性分析法的GC垃圾回收的效率较高,实现起来比较简单(引用计算法是算法简单,实现较难),但是其缺点在于GC期间,整个应用需要被挂起(STW,Stop-the-world,下面都简称STW),后面很多此类算法的提出,都是在解决这个问题(缩小 STW 时间)
- 在可达性分析类GC中,即使对象变成了垃圾,程序也无法立刻感知,直到 GC 执行前,始终都会有一部分内存空间被垃圾占用
- 基于引用计数法的GC,天然带有增量特性(就是GC过程中可能还会有新的GC产生),可与应用交替运行,不需要暂停应用;同时,在引用计数法中,每个对象始终都知道自己的被引用数,当计数器为0时,对象可以马上回收
- 上述两类GC各有千秋,总体来说,基于可达性分析的GC还是占据了主流,究其原因,首先,引用计数算法无法解决「循环引用无法回收」的问题,即两个对象互相引用,所以各对象的计数器的值都是 1,即使这些对象都成了垃圾(无外部引用),GC也无法将它们回收。当然上面这一点还不是引用计数法最大的弊端,引用计数算法最大的问题在于:计数器值的增减处理非常繁重,譬如对根对象的引用,此外,多个线程之间 共享对象时 需要对计数器 进行原子 递增/递减,这本身又带来了一系列新的复杂性和问题
阅读参考:垃圾标记阶段-引用计数算法、可达性分析算法
强引用、软引用、弱引用、虚引用
- 强引用,最传统的“引用”的定义,是指 在程序代码之中 普遍存在 的 引用赋值,即类似“
Object obj=new Object()
”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象 - 软引用 是用来描述一些 还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象 列进 回收范围之中 进行 第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用
- 弱引用 也是用来描述那些 非必须对象,但是 它的强度 比 软引用更弱一些,被弱引用关联的对象 只能生存到 下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用
- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象 是否有 虚引用的存在,完全不会对其 生存时间 构成影响,也无法通过 虚引用 来取得(get) 一个对象实例。为一个对象 设置 虚引用关联 的 唯一目的 只是为了 能在这个对象 被收集器回收时 收到一 个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用
分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”
分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储
显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间
如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用
在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”、“Major GC”、“Full GC”这样的回收类型的划分
也才能够 针对不同的区域 安排 与里面存储对象存亡特征 相匹配 的 垃圾收集算法
因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法
把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域
在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用
假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样
遍历整个老年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担
为了解决这个问题,就需要对分代收集理论添加第三条经验法则:
跨代引用假说(Intergenerational Reference Hypothesis):跨代引用 相对于 同代引用来说仅占极少数
这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的
举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了
依据这条假说,就不应再为了 少量的跨代引用 去扫描 整个老年代,也不必 浪费空间 专门记录 每一个对象是否存在 及 存在哪些跨代引用,只需 在新生代上 建立一个 全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构 把老年代 划分成 若干小块,标识出 老年代的哪一块内存 会存在 跨代引用
此后当发生Minor GC时,只有 包含了 跨代引用的小块内存里的对象 才会被加入到 GC Roots进行扫描
虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的
Minor GC、Major GC和Full GC
三种GC的区别
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集(包括 Eden 和 Survivor 区域), Minor GC会引发STW(Stop the World),暂停其他用户线程,等待垃圾回收结束,用户线程才会恢复运行
- 老年代收集(Major GC/Old GC):指 目标 只是老年代 的 垃圾收集,目前只有CMS收集器会有单独收集老年代的行为。出现Major GC,经常会伴随 至少一次的Minor GC
- 混合收集(Mixed GC):指 目标 是 收集 整个新生代 以及 部分老年代的垃圾收集,目前只有G1收集器会有这种行为
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集,包括年轻代和老年代
- Major GC比Minor GC慢10倍以上,STW时间更长
三种GC触发条件和解决办法(重要!!!)
- Minor GC:只有Eden区满的时候才会触发Minor GC,s0 或 s1 满的时候都不会触发GC
- Major GC:老年代空间不足时
- Full GC:
- 调用System.gc()
- 老年代空间不足,老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。 为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过
-Xmn
虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-XX:MaxTenuringThreshold
调大对象进入老年代的年龄,让对象在新生代多存活一段时间 - JDK1.7及以前的方法区(永久代)空间不足,在JDK 1.7及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。 为避免以上原因引起的 Full GC,可采用的方法为 增大永久代空间 或转为 使用CMS
- 通过Minor GC后 进入老年代的对象的平均大小 大于 老年代可用内存、从Eden区 直接把对象 复制到 老年代的时候 老年代可用空间 小于 该对象大小
- Concurrent Mode Failure,执行 CMS GC 的过程中 同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC
- 空间分配担保失败,使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC(和4应该是一个意思)
阅读参考
MinorGC、MajorGC和FullGC的对比
Stop the World-STW
- STW指的是GC事件发生过程中,会停止用户的所有线程,整个应用程序就像卡死一样,没有任何响应
- STW是为了确保对象可达性分析的准确性。如果对象在可达性分析过程中引用关系还在变化,则会导致分析的结果不准确
- STW与采用哪款GC器无关,所有的GC器都有这个事件
- STW是在JVM后台 自动发起 和 自动完成的
- 开发中不要使用System.gc(),会导致STW
System.gc()
- 显示调用System.gc() 会建议 垃圾收集器进行Full GC,注意只是建议,并不一定会真的进行Full GC
内存溢出和内存泄漏(重要)
内存溢出:内存都被对象占满了,没有足够空间分配给新的对象
内存泄露:内存泄漏 就是 申请了之后 没法释放(用完了不归还)
内存溢出要么是程序有问题导致的,要么就是分配的内存不够导致的
内存溢出出现的两个原因:
- 堆内存设置不够
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器回收(存在引用)
在抛出OOM之前,一般都会进行一次垃圾回收,尽可能的去清理出空间(例如尝试回收软引用)。当然也不是在任何情况下垃圾回收都会被触发。例如分配的对象的大小超过了堆的最大空间,就会直接抛出OOM
只有 对象 不会再被程序使用,但是 GC又不能回收 它们的情况,就叫内存泄漏,就是上面说的用完了不还
内存泄漏可能会导致内存溢出。当内存泄漏越来越多,逐步蚕食整个内存,直至耗尽所有内存,就会导致OOM
内存泄漏例子:
- 单例模式返回的对象 引用了另一个对象,被引用 的 对象 存在内存泄漏
- 数据库连接、IO连接的对象没有手动close,不能被回收,会存在内存泄漏
阅读参考
System.gc()的理解、内存溢出与内存泄漏、Stop the World
垃圾回收算法
从如何判定对象消亡的角度出发,垃圾收集算法 可以划分为 “引用计数式垃圾收集”(Reference Counting GC)和 “追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”
标记-清除算法(Mark-Sweep)
- 标记清除算法是最基础的垃圾回收算法,其过程分为 标记和 清除 两个阶段
- 在标记阶段标记所有需要回收的对象
- 在清除阶段清除可回收的对象并释放其所占用的内存空间
- 效率不算高(标记阶段 需要 递归遍历 找出 可达对象,清除阶段 需要 线性遍历堆中 所有对象,清除垃圾对象),如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低
- 在进行GC的时候,需要停止整个应用程序(STW),导致用户体验差
- 由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起 大对象 无法获得连续可用空间,而不得不 提前出发另一次垃圾收集动作
- 该算法一般在老年代中使用
标记-复制算法(Copying)
- 标记复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题
- 它将 可用内存 按容量 划分为 大小相等的两块,每次 只使用 其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
- 优点是没有标记和清除过程,实现简单,运行高效;复制之后保证了内存的连续可用,内存分配时也就不用再考虑内存碎片等复杂情况
- 缺点是只能有一半内存空间来被使用;复制对象之后,需要修改栈中对象的引用变量地址(因为对象被复制到了另外的空间),复制对象也会消耗不少的时间
- 如果内存中多数对象都是存活的,这种算法 将会产生 大量的 内存间复制 的 开销,但对于多数对象都是可回收的情况,算法 需要复制的 就是 占少数的存活对象,而且每次 都是针对 整个半区进行内存回收,分配内存时 也就不用考虑 有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可
- 此种方法被用于新生代的垃圾回收上,如果另外一块survivor空间 没有足够空间 存放上一次新生代收集下来的存活对象,这些对象将直接通过 分配担保机制 进入老年代
标记-整理算法(Mark-Compact)
- 标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的一端,然后再清理掉 端边界以外的内存区域
- 标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端
- 优点:被标记的存活的对象会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。当需要给新对象分配内存时,JVM只需要维护一个可用内存的起始地址就可以,比维护一个空闲列表少了许多开销
- 缺点:对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多,移动过程中需要STW,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行
- 该算法一般在老年代中使用
分代收集算法(Generational Collecting)
无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法
- 分代收集算法 根据对象的不同类型 将内存 划分为不同的区域,JVM将堆划分为新生代和老年代,新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。JVM根据不同的区域对象的特点选择了不同的算法
- 目前大部分JVM在新生代都采用复制算法,因为在新生代中每次进行垃圾回收时都有大量的对象被回收,需要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因此采用复制算法能安全、高效地回收新生代大量的短生命周期的对象并释放内存
- JVM将新生代进一步划分为一块较大的Eden区和两块较小的Servivor区,Servivor区又分为ServivorFrom区和ServivorTo区。JVM在运行过程中主要使用Eden区和ServivorFrom区,进行垃圾回收时会将在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,然后清理Eden区和ServivorFrom区的内存空间
- 老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记整理算法
- 在JVM中还有一个区域,即方法区(永久代),永久代用来存储Class类、常量、方法描述等。在永久代 主要回收 废弃的常量和无用的类
- JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,在少数情况下会被直接分配到老年代。在新生代的Eden区和ServivorFrom区的内存空间不足时会触发一次GC,该过程被称为MinorGC。在MinorGC后,在Eden区和ServivorFrom区中存活的对象会被复制到ServivorTo区,然后Eden区和ServivorFrom区被清理。如果此时在ServivorTo区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。若Servivor区的对象经过一次GC后仍然存活,则其年龄加 1。在默认情况下,对象在年龄达到15时,将被移到老年代
分区收集算法
- 分区算法 将整个堆空间 划分为 连续的 大小不同的 小区域(Region),对每个小区域都单独进行内存使用和垃圾回收,好处是 可以根据 每个小区域内存的大小 灵活使用 和 释放内存
- 分区收集算法 可以根据 系统可接受的停顿时间,每次快速回收若干个小区域的内存,以缩短 垃圾回收时系统停顿的时间,最后 以多次并行累加的方式 逐步完成 整个内存区域的垃圾回收
- 如果垃圾回收机制一次回收整个堆内存,则需要更长的系统停顿时间,长时间的系统停顿将影响系统运行的稳定性
- G1垃圾收集器采用的就是这种算法
增量收集算法(Incremental Collecting)
- 在上述的收集算法中,每次垃圾回收,应用程序都会处于一种Stop the World的状态,这种状态下,应用程序会被挂起,暂停一切正常的工作,这样一来,将严重影响用户体验或者系统稳定性
- 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替进行,垃圾回收线程 每次 只收集 一小片区域的内存空间,接着 切换到 用户线程继续执行,依次反复,直到垃圾收集完成
- 增量收集算法的基础 仍然是标记-清除 和 复制算法,增量收集算法 通过 对线程间冲突的 妥善处理,允许垃圾收集线程 以分阶段的方式 完成标记、清理或复制工作
- 缺点:线程切换 和 上下文转换的消耗,会使得垃圾回收整体成本上升,造成系统吞吐量的下降
阅读参考
- 垃圾清除阶段-标记-清除算法、复制算法、标记-压缩算法 包含3中算法的对比
- 分代收集算法、增量收集算法、分区收集算法简介
GC性能指标
- 吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间))
- 暂停时间,执行垃圾收集时,用户线程被暂停的时间
- 内存占用,Java堆所占的内存大小
随着内存的发展,占用更多的内存变得越来越能够容忍,这样一来,垃圾回收进行的次数少,程序运行时间就长,吞吐量就高;但是当需要进行垃圾回收的时候,由于需要清理的对象太多,用户线程暂停的时间就会很长,延迟就会很高
吞吐量 和 暂停时间 是相互矛盾的
追求高吞吐量,必然会降低内存回收的执行频率,但是这样,GC会需要更长的暂停时间来执行内存回收。
追求低延迟,也就是降低每次执行内存回收的暂停时间,必然会频繁的执行内存回收,这样会造成吞吐量降低,因为用户线程执行的时间变少了
阅读参考
垃圾回收器的分类、GC性能指标、吞吐量与暂停时间对比
几个概念(下面的垃圾收集器中会遇到)
并行-Parallel 与 并发(重要!!!)
- 垃圾回收并行,指的是 多条 垃圾收集(的) 线程 并行(在多个CPU上)工作,但此时 用户线程 处于等待状态
- 垃圾回收并发,指的是 用户线程 和 垃圾回收线程 同时执行,但不一定是并行的,可能会交替执行,垃圾回收线程在执行时 不会停顿 用户程序的执行
根节点 枚举(动词)
固定可作为GC Roots的节点 主要在 全局性的引用(例如常量或类静态属性)与 执行上下文(例如栈帧中的本地变量表)中,尽管目标明确,但 查找过程 要做到高效 并非一件容易的事情,现在Java应用越做越庞大,光是方法区的大小就常有数百上千兆,里面的类、常量等更是恒河沙数,若要逐个检查以这里为起源的引用肯定得消耗不少时间
迄今为止,所有收集器 在 根节点枚举 这一步骤时 都是必须暂停用户线程的,因此 毫无疑问 根节点枚举 与 之前提及的整理内存碎片 一样会面临 相似的“Stop The World”的困扰
现在 可达性分析算法 耗时最长 的 查找引用链的过程 已经可以做到 与 用户线程一起并发,但 根节点枚举 始终还是 必须在 一个 能保障一致性的快照中 才得以进行,这里“一致性”的意思是 整个枚举期间 执行子系统 看起来 就像被 冻结在某个时间点上,不会出现 分析过程中,根节点集合 的 对象引用关系 还在 不断变化的情况,若这点不能满足的话,分析结果准确性也就无法保证。这是导致 垃圾收集过程 必须停顿所有用户线程 的 其中一个重要原因,即使是 号称 停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,枚举根节点时也是必须要停顿的
由于目前主流Java虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完 所有执行上下文 和 全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的
在HotSpot的解决方案里,是 使用一组 称为 OopMap的数据结构来达到这个目的
一旦类加载动作完成的时候,HotSpot 就会把 对象内 什么偏移量上是什么类型的数据 计算出来,也会在 特定的位置 记录下 栈里和寄存器里 哪些位置是引用
这样收集器在扫描时 就可以 直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找
安全点
- 可达性分析 对 执行时间的敏感 体现在GC停顿上,因为这项分析工作 必须在一个能确保“一致性”的快照中进行,“一致性”的意思是指 整个分析期间 整个执行系统 看起来就像 被冻结在某个时间点上,不可以出现 分析过程中 对象引用关系 还在不断变化的情况,要不就会出现 分析结果准确定 无法得到保证
- 在某一瞬间,对JVM中所有的对象引用情况分析一遍是不现实的。在HotSpot中,使用一组称为OopMap的数据结构 来达到 “哪些地方存着对象引用” 这个目的。在 类加载完的时候,HotSpot就把 对象内 什么偏移量上 是什么数据类型 计算出来,在JIT编译过程中,也会在特定的位置(安全点)记录下 栈和寄存器中 哪些位置是引用
- 程序执行时 并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停
- 安全点选定标准:是否具有 让程序长时间执行 的特征。长时间执行最明显的特征:指令序列复用,例如方法调用、循环跳转、异常跳转等
- 另一个需要考虑的问题是,如何在GC发生时 让所有的线程都“跑”到最近的 安全点上 再停顿下来。两种方案:抢先式中断、主动式中断
- 抢先式中断:在GC发生时,把所以线程全部中断,如果发现有线程中断的地方 不在安全点上,就恢复线程让它“跑”到安全点上
- 主动式中断:当GC需要中断线程时,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志位真时就自己中断挂起。轮询标志的地方和安全点是重合的。
安全区域
- 安全点机制 保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点。但是在程序不执行的时候呢?不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,此时就需要安全区域来解决
- 安全区域 是指 在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的
- 线程执行到安全区域的代码时,首先标识自己已经进入了安全区域;在线程要离开安全区域时,要检查 系统是否已经完成了 根节点枚举,如果完成了,线程就继续执行,否则,就必须等待,知道收到可以安全离开 安全区域的信号为止
阅读参考
- 垃圾回收的并行与并发、安全点与安全区域
垃圾收集器
垃圾收集器分类
- 按垃圾收集器的线程数分类:分为 串行 和 并行 垃圾收集器,串行垃圾收集器 只有 一个垃圾回收线程;并行垃圾收集器 有多个 垃圾回收线程
- 串行以及并行垃圾收集器在回收垃圾的时候,都会Stop the World
- 按工作模式分类:分为 并发式 和 独占式 垃圾收集器,独占式 指的是 停止用户线程,直到垃圾回收完成;并发式 指的是 垃圾收集器线程 和 应用程序线程 交替工作,尽可能的减少应用程序的停顿时间
- 按碎片处理方式分类:分为 压缩式 和 非压缩式 垃圾收集器,压缩式 指的是 会对内存进行碎片整理;非压缩式 指的是 不对内存进行内存整理
- 对于经过内存碎片整理的空间,再分配对象空间的时候使用的是指针碰撞法;对于没有经过碎片整理的空间,再分配对象空间的时候使用的是空闲列表法
- 按工作内存区间分类:分为年轻代和老年代垃圾收集器
Serial垃圾收集器
特点
- 最基本,发展历史最悠久的收集器
- 单线程收集器,在它进行垃圾收集时,必须暂停其他所有的用户线程(STW),直到它收集结束
- 以 串行 方式执行
- 使用复制算法,用于新生代
- Serial垃圾收集器 是 JVM运行在Client模式下 的 新生代默认垃圾收集器
参数
-XX:+UseSerialGC
= Serial + SerialOld
优势
Serial有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比)
对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的
对于单核处理器 或 处理器核心数较少的环境来说,Serial收集器 由于没有 线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
应用场景
在用户桌面的应用场景以及部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择
Serial Old(老年代)垃圾收集器
特点
- Serail收集器的老年代版本
- 单线程收集器,“单线程”的意义不仅是 只会使用一个处理器 或 一条收集线程 去完成 垃圾收集工作,更重要的是强调在 它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束
- 以 串行 方式执行
- 使用标记-整理算法,用于老年代
- Serial Old 是 JVM运行在Client模式下 的 老年代默认垃圾收集器
- 在HotSpot虚拟机的Server模式下,Serial Old有两个用途:
- 在JDK 5以及之前的版本中与新生代的Parallel Scavenge配合使用
- 作为老年代CMS收集器的后备垃圾收集方案,在并发收集发生Concurrent Mode Failure时使用
Serial+ Serial Old组合收集器运行示意图如下:
参数
-XX:+UseSerialGC
= Serial + SerialOld
Par(Parallel)New(New表示只能处理新生代)垃圾收集器
特点
- ParNew 是 Serial收集器 的 多线程版本,虽然是多线程,但仍需要STW
- 收集器可用的控制参数、收集算法(复制)、STW、对象分配规则、回收策略等于Serial完全一样
- 以 并行 方式执行(多个GC线程)
- 使用复制算法,用于新生代,垃圾回收次数比较频繁,使用 并行(条垃圾收集器线程) 回收的方式,效率高
- ParNew 是 激活CMS后(使用
-XX:+UseConcMarkSweepGC
) 的 默认新生代收集器 - ParNew 是 Server模式下的 虚拟机首选 新生代收集器
- ParNew垃圾收集器 默认开启 与 CPU同等数量 的线程 进行垃圾回收
ParNew + Serial Old组合收集器运行示意图如下:
参数
-XX:+UseParNewGC
= ParNew + Serilal Old,指定使用ParNew- 在Java应用启动时可通过
-XX:ParallelGCThreads
参数调节垃圾收集的线程数量 -XX:+UseConcMarkSweepGC
,指定使用CMS后,会默认使用ParNew作为新生代收集器
应用场景
- 目前除Serial外,只有 ParNew 能与 CMS(在老年代)收集器 配合工作
- 在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销
为什么只有ParNew能与CMS收集器配合?
- CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
- CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作,因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现,而其余几种收集器则共用了部分的框架代码
Parallel Scavenge垃圾收集器(吞吐量优先收集器,新生代)
特点
- Parallel Scavenge收集器 是为提高 新生代垃圾收集效率 而设计的 垃圾收集器,它的关注点 是 达到一个 可控制的吞吐量(上面已经整理过);而 CMS等收集器的关注点 是 尽可能地 缩短 垃圾收集时 用户线程的停顿时间(不同点)
- 多线程收集器,虽然是多线程,但仍需要STW
- 以 并行 方式执行(多个GC线程)
- 使用复制算法,用于新生代
- Parallel Scavenge 是 Server模式下的 虚拟机默认垃圾收集器
参数
-XX:MaxGCPauseMillis
,控制 最大 垃圾收集停顿时间,一个大于0的毫秒数,-XX:MaxGCPauseMillis
设置的稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降,因为可能导致垃圾收集发生得更频繁-XX:GCTimeRatio
,控制吞吐量大小,大于0小于100的整数,是垃圾收集时间 占总时间的比率-XX:+UseAdaptiveSizePolicy
自适应调节策略开启,当这个参数打开之后,不需要手工指定 新生代大小、Eden与Survivor比例、晋升到老年代对象年龄代数。JVM会根据当前系统的运行情况收集信息,动态调整以提供最合适的 停顿时间 或 最大吞吐量,这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)-XX:+UseParallelGC(默认)
= Parallel Scavenge + Parallel Old(JDK 1.8默认)
应用场景
停顿时间越短 就越适合 需要与用户交互的程序,良好的响应速度能提升用户体验
而 高吞吐量 则可以 高效率地利用CPU时间(减少垃圾回收所耗费的时间),让用户代码获得更长的运行时间
Parallel Scavenge适合那种交互少、运算多的场景,例如:执行批量处理、订单处理、工资支付、科学计算的应用程序
Parallel Old垃圾收集器
特点
- Parallel Scavenge收集器的老年代版本
- 多线程收集器,虽然是多线程,但仍需要STW
- 以 并行 方式执行(多个GC线程)
- 使用标记-整理算法
Parallel Old垃圾收集器 在设计上 优先考虑系统吞吐量,其次考虑停顿时间等因素
如果系统对吞吐量的要求较高,则可以优先考虑新生代的Parallel Scavenge垃圾收集器 + 老年代的Parallel Old垃圾收集器的配合使用,如下图:
参数
-XX:+UseParallelOldGC
= Parallel Scavenge + Parallel Old(该参数在JDK1.5之后已无用)-XX:-UseParallelOldGC
= Parallel Scavenge + Serial Old
注意参数一个是+,一个是-,对应的是老年代使用的收集器不一样
CMS(Concurrent Mark Sweep)垃圾收集器(重要!!!)
特点
- 为 老年代 设计的 垃圾收集器,主要目的是达到 获取 最短回收停顿时间(低延迟) ,以便 在多线程并发环境下 以最短的垃圾收集停顿时间 提高系统的稳定性
- 多线程收集器
- 在并发标记 和 并发清除阶段 以 并发 方式执行(GC线程 和 用户线程 同时进行);在重新标记阶段 以 并行 方式执行(多个GC线程)
- 使用标记-清除算法,不进行整理压缩,会产生内存碎片,用于老年代
- CMS 只能和 Serial收集器 以及 ParNew收集器组合使用,这两个都是新生代的收集器
- CMS不能等到内存空间不够的时候,再去进行垃圾回收,需要提前进行垃圾回收。因为垃圾回收的时候和用户线程并发执行,如果,内存空间不够了,再去进行垃圾回收,用户线程和垃圾回收线程都没有足够的空间可用了
缺点(重要!!!)
- 对CPU资源非常敏感,并发标记 虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数 是 (cpu个数+3)/4。当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS),类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间,但效果并不理想,JDK1.6后就官方不再提倡用户使用
- 无法处理浮动垃圾,浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,CMS无法在 当次收集中 处理掉。由于在并发清除阶段用户线程还需要运行,使得并发清除时需要预留一定的内存空间,意味着CMS不能像其他收集器在老年代几乎填满再进行收集(也要可以认为CMS所需要的空间比其他垃圾收集器大)。可以通过
-XX:CMSInitiatingOccupancyFraction
设置CMS预留内存空间,JDK1.5默认值为68%、JDK1.6变为大约92% - Concurrent Mode Failure失败, 接着上面说到的预留一定的内存空间,如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败,这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生,这样的代价是很大的,所以
-XX:CMSInitiatingOccupancyFraction
不能设置得太大 - 产生大量内存碎片,这是由于CMS基于"标记-清除"算法,清除后不进行压缩操作。由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大。解决内存碎片办法:1
.-XX:+UseCMSCompactAtFullCollection
, 使得CMS出现上面情况时不进行Full GC,而开启内存碎片的合并整理过程,但合并整理过程无法并发,停顿时间会变长,默认开启(但不会进行,结合CMSFullGCsBeforeCompaction);2.-XX:+CMSFullGCsBeforeCompaction
,设置 执行多少次不压缩的Full GC后,来一次压缩整理,为减少 合并整理过程的停顿时间,默认为0,也就是说每次都执行Full GC,不会进行压缩整理
参数
-XX:+UseConc(current)MarkSweepGC
= ParNew +CMS + SerialOld(给CMS备份用),手动指定使用CMS,开启该参数后会自动将-XX:+UseParNewGC
打开-XX:CMSInitiatingOccupancyFraction
堆内存使用率阈值,一旦达到该阈值,便开始回收处理,如果内存增长缓慢,可以设置的较高,降低CMS触发频率;如果内存增长较快,可以设置的较低,避免 频发触发 老年代 串行收集器-XX:+UseCMSCompactAtFullCollection
,默认开启,用于在CMS收集器处理过程中开启内存碎片整理(默认是开启的,此参数从JDK 9开始废弃)-XX:CMSFullGCsBeforeCompaction
,用于 执行了多少次 不压缩的Full GC后,跟着来一次带压缩的,默认值为0,表示每次进入Full GC时都进行碎片整理(此参数从JDK 9开始废弃)-XX:ParallelCMSThreads
回收线程数
应用场景
适用于与用户交互较多的场景,希望系统停顿时间最短,注重服务的响应速度,以给用户带来较好的体验,如常见WEB、B/S系统的服务器上的应用
CMS回收过程
- 初始标记:仅仅只是标记出和GC Roots 直接关联 的对象,速度很快,单线程执行,需要STW
- 并发标记:从GC Roots 的 直接关联对象 开始 遍历 整个对象图的过程,这个过程耗时长,不需要停顿用户线程
- 重新标记 (多GC线程并行执行):由于 在并发标记过程中 用户线程继续运行,导致在垃圾回收过程中部分对象的状态发生变化,为了确保这部分对象的状态正确性,需要对其(状态发生变化的部分对象)重新标记并STW,这个阶段的停顿时间 比 初始标记 稍长,但远比 并发标记 短。重新标记 就是为了 修正 在并发标记期间 因用户程序继续运作 而导致 标记产生变动 的 那一部分对象 的 标记记录。参考阅读上面的“增量更新”
- 并发清除:和用户线程一起工作,执行清除GC Roots不可达对象的任务,由于 不需要移动 存活对象(会产生内存碎片),所以也不需要暂停用户线程
CMS垃圾收集器在和用户线程一起工作时(并发标记和并发清除)不需要暂停用户线程,有效缩短了垃圾回收时系统的停顿时间,同时由于CMS垃圾收集器和用户线程一起工作,因此其并行度和效率也有很大提升
G1(Garbage-First)垃圾收集器(重要!!!)
特点
- G1(Garbage-First)是JDK7-u4才推出商用的收集器
- 面向服务端应用(针对具有大内存、多处理器的机器)的垃圾收集器
- G1 与前面的垃圾收集器有很大不同,它把新生代、老年代的划分取消了!
- G1 将堆内存 划分为 很多不相关的区域-Region(物理上可以是不连续的空间),仅是从概念保留了年轻代和老年代,实际上使用不同的Region来表示Eden,S0区,S1区,老年代,每个Region都可能随G1的运行在不同代之间切换,涉及Region的内容在下面还有
- G1 有计划的 避免 在整个Java堆中 进行 全区域的 垃圾收集,G1会跟踪 各个Region里面的 垃圾堆积的价值大小(回收所获得的空间大小 以及 回收所需要的时间 这两个经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
- 每个Region都有一个 Remembered Set,用来记录 该Region中的对象 的 引用对象 所在的 Region,通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描
参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis
,停顿时间,默认200ms,如果把停顿时间调得非常低,譬如设置为20毫秒,很可能出现的结果就是 由于 停顿目标时间太短,导致 每次选出来的回收集 只占堆内存很小的一部分,收集器 收集的速度 逐渐 跟不上 分配器 分配的速度,导致垃圾慢慢堆积。很可能一开始 收集器还能从 空闲的堆内存中 获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的-XX:ParallelGCThreads
,设置 在STW的时候 并行的垃圾收集线程数量-XX:ConcGCThreads
设置 用户线程 与 垃圾收集线程 并发的时候,垃圾收集线程的线程数量-XX:InitiatingHeapOccupancyPercent
设置 触发 并发GC周期(不是普通的GC) 的 Java堆占用率的阈值,超过此值,就触发GC,默认是45-XX:G1HeapRegionSize=n
可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区-XX:G1NewSizePercent
、XX:G1MaxNewSizePercent
年轻代内存会在 初始空间-XX:G1NewSizePercent
(默认整堆5%) 与 最大空间-XX:G1MaxNewSizePercent
(默认60%) 之间动态变化,且由参数 目标暂停时间-XX:MaxGCPauseMillis
(默认200ms)、需要扩缩容的大小 以及 分区的已记忆集合(RSet)计算得到- G1依然可以设置固定的年轻代大小(参数
-XX:NewRatio、-Xmn
),但同时 暂停时间-XX:MaxGCPauseMillis
将失去意义
优势
- 并行与并发:多个垃圾收集线程同时工作(并行),垃圾收集线程 与 用户线程交替执行(并发),其实CMS也有这个优势
- 分代收集:G1将堆空间划分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。它不要求年轻代,老年代是连续的。同时,G1同时兼顾年轻代和老年代的垃圾回收,其他的垃圾收集器,要么工作在年轻代,要么工作在老年代
- 空间整合(内存碎片整理):G1内存回收使用Region作为基本单位,Region之间使用的是复制算法,能够对内存空间进行整理
- 可预测的停顿时间模型:可以让用户 明确指定 在一个长度为M毫秒 的 时间片段内,消耗在 垃圾收集上的时间 不得超过 N毫秒
- G1从整体来看 是基于 “标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集
应用场景
- 面向服务端应用,针对具有大内存、多处理器的机器,最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案。如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒
- 用来替换掉JDK1.5中的CMS收集器
- 在下面的情况时,使用G1可能比CMS好:
- 超过50%的Java堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
- GC停顿时间过长(长于0.5至1秒)
G1回收过程
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Top at Mark Start)指针的值,让下一阶段 用户线程 并发运行时,能正确地 在可用的Region中 分配 新对象。这个阶段需要STW,但耗时很短,而且是借用 进行Minor GC的时候 同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿
- 并发标记(Concurrent Marking):从GC Root开始对堆(刚才产生的Region集合)中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB(Snapshot-At-The-Beginning,原始快照)记录下的 在并发时 有引用变动的对象
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停STW,用于处理 并发标记阶段结束后 仍遗留下来的 最后那少量的SATB(Snapshot-At-The-Beginning,原始快照)记录。上一阶段(并发标记)中 对象的变化 记录在线程的Remembered Set Log中,这里(最终标记)把Remembered Set Log合并到Remembered Set中。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象 复制到 空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程STW,由多条收集器线程并行完成的
综上:G1收集器除了并发标记外,其余阶段都是要完全暂停用户线程的
G1和CMS比较-待完善(重要!!!)
- 相比于CMS,G1还不具备全方位、压倒性优势。比如,G1为了垃圾收集产生的内存占用 以及 程序运行时的额外执行负载 都比CMS要高
- 就内存占用来说,虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的
- 在执行负载的角度上,同样由于两个收集器各自的细节实现特点导致了用户程序运行时的负载会有不同,譬如都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理
阅读参考
- 7款经典的垃圾收集器以及它们之间的组合关系、如何查看默认的垃圾回收器(包括垃圾收集器之间组合关系 和 查看默认的垃圾收集器)
- Serial与Serial Old收集器、ParNew收集器、Paralell Scavenge与Parallel Old收集器、CMS收集器 其中有“如何选择Serial GC和Parallel GC和CMS GC?”
- G1垃圾收集器、优势与缺点、参数设置、使用场景 其中有"G1使用场景"
垃圾收集器总结
新生代收集器/老年代收集器
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1
吞吐量优先、停顿时间优先
吞吐量优先:Parallel Scavenge收集器、Parallel Old收集器
停顿时间优先:CMS(Concurrent Mark-Sweep)收集器
串行、并行、并发(重要!!!)
串行:Serial、Serial Old
并行:ParNew、Parallel Scavenge、Parallel Old
并发:CMS、G1
回收算法
新生代的垃圾收集器都是使用的复制算法
复制算法:Serial、ParNew、Parallel Scavenge、G1
老年代的垃圾收集器使用的算法是 标记-整理 和 标记-清除算法(只有CMS)
标记-清除:CMS
标记-整理:Serial Old、Parallel Old、G1
G1既回收新生代又回收老年代,所以使用的是复制算法 和 标记-整理算法
JDK默认垃圾收集器(重要!!!)
可以通过java -XX:+PrintCommandLineFlags -version
查看
- jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
- jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
- jdk1.9 默认垃圾收集器G1
表格
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理算法 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并行、并发 | 老年代 | 标记-清除算法 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发、并发 | 新生代 + 老年代 | 标记-整理 + 标记-清除算法 | 响应速度优先 | 面向服务端应用,替换CMS |
垃圾收集器参数总结
如何选择垃圾收集器(重要!!!)
如果是数据分析、科学计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点
如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点
如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的
垃圾回收器总结、怎么选择垃圾收集器
回收方法区(重要!!!)
- 主要回收两部分内容:废弃常量 + 无用的类
- 判断一个常量 是否是 废弃常量,只需要看 是否有任何对象 引用 当前这个常量
- 判断一个类 是否是 无用的类,条件很苛刻:
- 该类的 所有实例 都已经被回收,Java堆中 不存在 该类及其任何派生子类 的 实例
- 加载该类的 ClassLoader类加载器 已经被回收,这个条件 除非是 经过精心设计的 可替换类加载器 的 场景,如OSGi、JSP的重加载等,否则通常是很难达成的
- 该类 对应的 java.lang.Class对象 没有在任何地方被引用,无法在任何地方 通过反射 访问该类的方法
方法区相关参数
-Xnoclassgc
,关闭虚拟机对class的垃圾回收功能-verbose:class
,查看在程序运行的时候有多少类被加载-XX:+TraceClassLoading
,动态追踪类加载-XX:+TraceClassUnLoading
,动态追踪类卸载
阅读参考:方法区的垃圾回收
GC日志
GC日志相关参数
-XX:+PrintGC
、-Xlog:gc
输出GC日志-XX:+PrintGCDetails
、-Xlog:gc*
, 输出GC详细日志-XX:+PrintGCTimeStamps
输出GC的时间戳,基准时间形式-XX:+PrintGCDateStamps
输出GC的时间戳,日期形式-XX:+PrintHeapAtGC
、-Xlog:gc+heap=debug
在GC进行的前后打印堆日志-Xloggc:../logs/gc.log
日志文件的输出路径-XX:+PrintGCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime
、-Xlog:safepoint
查看GC过程中用户线程并发时间以及停顿的时间-XX:+PrintAdaptive-SizePolicy
、-Xlog:gc+ergo*=trace
查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息-XX:+PrintTenuring-Distribution
、-Xlog:gc+age=trace
查看熬过收集后剩余对象的年龄分布信息
GC日志内容
- [GC/[Full GC,说明垃圾收集的停顿类型
- [DefNew、[Tenured、[Prem,代表GC发生的区域
- Serial在新生代收集器的名字是Default New Generation,因此显示的是DefNew
- 使用ParNew收集器在新生代的名字会变为ParNew
- 使用Parallel Scavenge收集器在新生代的名字是PSYoungGen
- 老年代收集 和 新生代收集的道理一样,名字也是收集器决定的
- G1收集器 会显式为 garbage-first heap
- Metaspace 元数据区
- 方括号内部:GC前 该内存区域 已使用容量 -> GC后 该内存区域 已使用区域(该区域内存总量)
- 方括号外部:GC前 java堆 已使用容量 -> GC后 java堆 已使用容量(java堆总容量)
- 最后的数字表明 该内存区域 GC所占用的时间,单位是s
- user,垃圾收集器 花费的 所有cpu时间
- sys,花费在 等待系统调用 或 系统事件的时间
- real,真正GC从开始到结束的时间
阅读参考:常用的显示GC日志的参数、GC日志分析、日志分析工具的使用
JVM面试(四)-垃圾回收、垃圾收集器、GC日志相关推荐
- java eden space_《深入理解Java虚拟机》(六)堆内存使用分析,垃圾收集器 GC 日志解读...
堆内存使用分析,垃圾收集器 GC 日志解读 重要的东东 在Java中,对象实例都是在堆上创建.一些类信息,常量,静态变量等存储在方法区.堆和方法区都是线程共享的. GC机制是由JVM提供,用来清理需要 ...
- 【JVM】Java垃圾回收机制(GC)详解
Java垃圾回收机制(GC)详解 一.为什么需要垃圾回收? 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配不回收,但是事实并非如 ...
- 深入理解Java垃圾回收——垃圾收集器
<深入理解Java垃圾回收--虚拟机高效回收的背后>讲述了垃圾回收的理论思想,本篇文章来深入了解垃圾回收的实践:垃圾收集器. 在讲解垃圾收集器之前必要要统一几点认知: 1.用户线程:执行应 ...
- JVM内存区域(Java内存区域)、JVM垃圾回收机制(GC)初探
一.JVM内存区域(Java内存区域) 首先区分一下JVM内存区域(Java内存区域)和Java内存模型(JMM)的概念.Java线程之间的通信采用的是共享内存模型,这里提到的共享内存模型指的就是Ja ...
- Java Jvm 中的垃圾回收机制中的思想与算法 《对Java的分析总结》-四
Java中的垃圾回收机制中的思想与算法 <对Java的分析总结>-四 垃圾回收机制 中的思想与算法 引用计算法 给对象中添加一个引用计数器,每当一个地方引用它的时候就将计数器加1,当引用失 ...
- JVM之垃圾回收-垃圾收集算法
JVM之垃圾回收-垃圾收集算法 如何判断对象是否存活 引用计数算法 可达性分析(GC Roots Tracing)算法 效率 对象之间相互循环引用的问题 使用引用计数算法 使用可达性算法 Java引用 ...
- java gc回收堆还是栈_浅析JAVA的垃圾回收机制(GC)
1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制. 注意:垃圾回 ...
- 20191212浅析JAVA的垃圾回收机制(GC)
1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制. 注意:垃圾回 ...
- jvm内存与垃圾回收重点总结
文章目录 一.jvm简介 1.jvm的位置 2.JVM的整体结构 3.java代码执行流程 二.类加载子系统 1.类的加载过程 2.类加载器分类 ⭐3.双亲委派机制 三.运行时数据区及线程 四.程序计 ...
最新文章
- 从别人那拷下来的几点Session使用的经验(转载)
- BZOJ-1798 维护序列
- 关于话题演化关系网络生成的路线思考:从话题聚类到话题网络展示
- Vue 模块化开发(构建项目常用工具)
- Yuchuan_Linux_C编程之二 GCC编译
- easyui layout 收缩的bug
- c++ vector排序_个性化推荐系统源代码之基于LR模型的推荐系统离线排序方案
- 买不到口罩怎么办?Python 爬虫帮你时刻盯着自动下单!| 原力计划
- android http封装类,HTTP封装类 工具类 For Android
- 前景检测算法(二)--codebook和平均背景法
- Java中的方法的重载
- 计算机毕业设计之会议预约系统设计与实现
- 微信小程序性能优化实用建议
- BottomNavigationView动态添加MenuItem
- 【CSS】美化网页元素+盒子模型
- HBuilderX快速上手
- “IND-”安全概念的简单解释(IND-CPA,IND-CCA等)
- WLAN技术之WLAN安全
- C语言简介之进制转换,原码、反码、补码,位运算符,函数
- 2017 计蒜之道 初赛 第一场 A、B题
热门文章
- 爱快路由研究中关于rootfs.gz文件的解压缩问题
- c语言中缺少函数标题,error C2332: “struct”: 缺少标记名
- 浅谈Oracle RAC --集群管理软件GI
- 1740 蜂巢迷宫(模拟,暴力,剪枝)
- 查找算法--Java实例/原理
- 计算机的绝密历史——窃取的创意、专利战争和丑闻如何改变了世界……
- 2.1 电子计算机的兴起
- 【java】IDEA设置自己的名字和时间(Created by)
- 计算机比赛小组名称和口号,小组名称和口号大全励志,小组名称和口号大全励志...
- 怎么把虚拟机的计算机弄到桌面,Win10自带的虚拟机功能,教你这样设置,免费让一台电脑变3台!...