JVM GC
本文最后更新于:2022年7月28日 下午
概览:JVM GC
预警!仅用于本人快速自学,不过欢迎指正。
内存区域与垃圾回收
Java运行时内存区域:程序计数器、虚拟机栈、本地方法栈、堆和方法区。
- 程序计数器、虚拟机栈、本地方法栈这三者线程私有,与线程的生命周期相同。栈中的栈帧随着方法的进入和退出而入栈出栈,每一个栈中分配多少内存基本上是在类结构确定下来时就已知了,这几个区域分配回收具有确定性。
- 但是堆和方法区,具有不确定性:一个接口的多个实现类需要的内存可能不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有运行期间,才能知道。这一部分是需要垃圾回收所重点关注的。
对象是否存活
1.引用计数法,缺陷:循环引用问题
可达性分析算法
实现:找到一系列的GC Roots节点,作为起始节点,沿着引用关系搜索,搜索所走过的路径称为引用路径,如何一个对象到GC Roots之间没有引用路径,那就说明对象不再被使用。
JVM GC Roots对象
- 虚拟机栈中的引用对象,eg:方法参数、局部变量、临时变量
- 方法区中的静态属性引用,例如Java中的引用类型静态变量
- 方法区常量引用的对象,例如字符串常量池的引用
- java虚拟机内部的引用,例如系统类加载器
- 被同步锁持有的对象
- 本地方法栈中JNI引用的对象
- 等
四大引用
强、软、弱、虚
- 强:传统引用
- 软:SoftReference,系统将要发生内存溢出前进行标记回收
- 弱:WeakReference,关联对象存活到下次垃圾回收
- 虚:PhantomReference,目的:在关联对象被回收时获得通知
直接内存的回收,Cleaner 继承了 PhantomReference虚引用。
方法区的回收
虚拟机规范中不强制。
方法区回收内容:废弃的常量和不再使用的类型。
类型不再使用的条件:
- 该类的所有实例都已经被回收。堆中不存在该类和派生子类。
- 类的加载器被回收。——最好是类加载器可替换的场景,否则难以达到。
- Class对象没有任何对象引用。
在大量使用反射、动态代理、CGLib等字节码框架这类频繁自定义类加载器的场景,通常都需要Java虚拟机具备类型卸载的能力。
垃圾收集算法
分代收集
经验法则一:绝大多数对象朝生夕灭。
经验法则二:活过多次垃圾回收的对象更容易存活。
由经验法则引发出的设计原则:堆应该划分不同的区域,回收对象按照年龄分配到不同区域存储。
由此有了新生代和老年代,新生代中每次回收都有大批对象死去,而存活的对象会逐步晋升到老年代之中。
划分区域之后,垃圾收集器可以每次只收集某一部分的垃圾,但是有一个问题:例如,只想回收新生代的垃圾,但是有可能老年代引用了新生代,这样的话回收也必须扫描老年代进行可达性分析。
经验法则三:跨代引用相对于同代引用来说占比极少。
根据这条法则,在新生代创建一个全局的数据结构——记忆集,标识出老年代的哪一块内存存在跨代引用,当发生新生代的MinorGC时,只扫描有跨代引用的区域。
标记-清除
两阶段:
- 标记出所有需要回收的对象(或者标记存活对象)
- 统一回收所有被标记的对象
缺点:
- 执行效率不稳定,标记和清除随着对象数量增长而效率降低
- 内存空间碎片化问题,会产生大量内存碎片
标记-复制
将可用内存分为容量相等的两块,每次只使用其中的一块,当一块用完时,将还存活的对象移动到另一块,再把已经使用过的清理掉。
优点:实现简单,不用考虑空间碎片。
缺点:空间浪费,在对象存活率较高的时候会进行太多的内存复制,老年代最好不要使用。
目前,主流虚拟机都使用它来回收新生代。
HotSpot虚拟机将新生代分为Eden区、Survivor From区、Survivor To区,8 :1:1。
当Survivor 区不足够容纳一次MinorGC时,使用老年代来分配担保。
标记-整理
针对老年代的垃圾回收,提出的算法。
- 标记存活对象
- 将所有存活的对象向内存的一端移动,然后清理掉边界意外的内存。
问题:移动存活对象 —— 需要更新引用,必须全程暂停用户程序才能够使用, —— 称为 Stop The World。
垃圾收集器
垃圾收集不可知、 不可控
1. serial 收集器 与 serial-old
单线程收集器, —— 进行垃圾收集的时候,必须暂停其他工作线程,直到收集结束。
- 新生代采取复制算法,暂停所有线程
- 老年代采取标记-整理算法,暂停所有线程
适用场景:客户端模式下,单核处理器或者处理器核心比较少。
2. ParNew
serial收集器的多线程版本,GC时多线程进行处理。
3. Parallel Scavenge收集器 与 Parallel -old
新生代收集器,基于标记-复制算法,并行的多线程收集器。
- 可控制吞吐量:控制用户代码时间 与 处理器总时间比值。
老年代收集器,支持多线程并发收集,基于标记-整理
4.CMS收集器
CMS —- Concurrent Mark Sweep
目的:获取最短回收停顿时间。
采用:标记-清除算法。
运行过程
- 初始标记,STW,快速标记GC Roots关联的直接对象
- 并发标记,从
GC Roots关联的直接对象
查找对象图中的垃圾,耗时时间长,但是GC线程与用户线程并发执行。 - 重新标记,STW,修正并发标记期间,标记变动的那一部分,耗时短。
- 并发清除,清理删除标记阶段已经死亡的对象,GC线程与用户线程并发执行
缺点
在并发阶段会占用处理器的一部分资源,导致应用程序变慢,降低总吞吐量。
无法处理浮动垃圾,可能会引起Full GC 和 STW。
浮动垃圾:在并发标记与并发清除期间产生的新垃圾这一次GC中不能回收,称之为浮动垃圾。为了确保有足够的空间,CMS不能在快满的时候进行收集,而是达到了一定比例就需要收集。如果预留的空间无法满足程序分配,就会出现并发失败现象,这是只能使用STW,然后Serial-old收集器来进行老年代垃圾回收,这样的停顿时间会很长。
基于标记-清除垃圾算法,会产生大量内存碎片。
5. G1收集器
G1 —- Garbage First收集器
里程碑式的垃圾收集器 —— 局部收集、基于Region的内存布局。
前面的收集器:分代思想来做处理。
G1: 面向堆内存的任何部分来组成回收集,虽保留新生代、老年代的概念,但区域不再固定,而是集合。
将Java堆分成大小相等的独立区域Region,此外还有区域专门存储大对象 —— 超过Region一半的区域。
其他:新生代-老年代比例划分
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM老年代和新生代的比例 - 一中晴哥威武 - 博客园 (cnblogs.com)
其他:G1收集器的适用场景?
面向服务端应用,针对具有大内存、多处理器的机器。
最主要的应用是需要低 GC 延迟,并具有大堆的应用程序提供解决方案。
如:在堆大小约 6GB 或更大时,可预测的暂停时间可以低于 0.5秒;(G1 通过每次只清理一部分而不是全部的 Region 的增量式清理来保证每次 GC 停顿时间不会过长)。
用来替换掉 JDK1.5 中的 CMS 收集器,在下面的情况时,使用 G1 可能比 CMS 好。
- 超过 50% 的 Java 堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
- GC停顿时间过长(长于0.5至1秒)
HotSpot 垃圾收集器里,除了 G1 以外,其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,即当 JVM 的 GC 线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
原文链接:https://blog.csdn.net/chengqiuming/article/details/119271564
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!