JVM—垃圾回收与内存分配

本文探讨了垃圾回收与内存分配的重要性,涉及对象生存分析、引用计数与可达性算法,以及各类垃圾收集器(如Serial、ParNew、CMS和G1)的工作原理和优缺点。重点讲解了分代理论和不同收集器在并发、停顿时间和内存效率上的策略。
摘要由CSDN通过智能技术生成

1. 垃圾回收与内存分配

为啥要了解垃圾回收集和内存分配?

排查各种内存溢出内存泄漏以及达到高并发的瓶颈时,需要对这些自动化的技术进行必要的手动调节与监控。

要解决的三个问题

  • Who—哪些对象需要回收?
  • When—什么时候回收?
  • How—如何回收?

内存分配与回收的主要战场

线程共享的部分

  • Java堆
  • 方法区

因为太多不确定性,运行时才知道创建哪些对象,创建多少个对象。

而线程私有的部分,需要的内存在编译的时候就确定了,不需要过多的干预。

2. 对象你死没死?

1.引用计数算法

给予引用时+1,

引用失效时-1,

为0视为该对象已死

简单,但Java不用,因为需要考虑很多额外的情况,例如相互循环引用

2.可达性分析算法

通过一系列GC Roots为根对象的树,从GC Roots出发,搜索对象是否可达(不可达的就是那些根不是GC Roots的树)

不可达的,就说明对象进入缓刑,再给一次执行finalize()方法的自我救赎的机会。

如果没抓住机会(上链),就执行死刑。

image-20210623103654911

对象的引用

JDK1.2之前,对象有两种状态:

  • 被引用
  • 未被引用

之后,四种状态

  • 强引用

    不回收

  • 软引用

    内存实在不够了,再回收

  • 弱引用

    要回收的时候就回收

  • 虚引用

    肯定会回收,虚引用只是方便通知这个对象而已,其他没啥用,也没办法用这个引用找到该对象。

3. 垃圾回收算法

大体可分为两类:

  • 引用计数式垃圾收集(直接垃圾收集)
  • 追踪式垃圾收集(间接垃圾收集)

分代假说

  • 弱分代假说:绝大多数对象都是朝生夕灭的(命短)。
  • 强分代假说:熬过越多次垃圾回收的对象就越难以消亡(打不死的小强,老油条)。
  • 跨代引用假说:老年代带新生代,新生代会逐渐晋升为老年代(师傅带徒弟)。

因此,Java堆要划分区域,以便更好的管理,同时兼具了垃圾回收的时间开销与内存有效利用。

  • 新生代区域

    每次只关注如何保留少量对象存活,而不是去标记大量将要被回收的对象(挑强壮的)。

  • 老年代区域

    较低频率去扫描(管得松点)。

3.1 标记—清除算法

人如其名,先标记,后清除,标记待清除对象或反之都行。

缺点:

  • 执行效率不稳定

    对象越多,效率越低

  • 内存空间碎片化问题

3.2 标记—复制算法

可用内存,一分为二,一半用来使用,另一半用来存放存活下来的对象(复制过去)

优点:

  • 解决了内存空间碎片化的问题

缺点:

  • 如果大量的对象存活了下来,那么复制的开销会很大
  • 太浪费空间,可使用空间只有原来的一半

现有一些解决方案,如“Appel式回收”,把新生代划分为Eden和两个Survivor,比例8:1。

但这也需要有“担保人”去担保(万一Survivor不够了,让担保人出资)。

3.3 标记—整理算法

与标记—清除算法类似,都需要先做标记。

不同的是,把要存活的对象移动到内存区域的一端,然后清除边界外的对象。

优点:

  • 也解决了空间碎片化的问题

缺点:

  • 存活对象多时,移动操作极为负重(移动时需要在这停顿

移动还是不移动,各有优缺点。

  • 移动 ,在当前垃圾回收时难,在以后内存访问时爽。(先苦后甜)

  • 不移动,在当前垃圾回收时爽,在以后内存访问时难。(先甜后苦)

也有**“和稀泥”**的解决方案:先采用标记—清除的算法,等到空间碎片化过于严重时,再采用标记—整理的算法。

先忍着不整理,乱到不行了,看着难受了,再整理。

4. 经典的垃圾收集器

4.1 Serial收集器

类型

新生代收集器

最基础、历史最悠久的收集器。

  • 缺点:“Stop The World!”

单线程工作,垃圾收集时,需要暂停其他所有的工作线程,体验太差。

工作模式
  • 新生代采用:复制算法
  • 老年代采用:标记—整理算法(Serial Old的工作)
应用场景

客户端模式下的虚拟机(分配管理的内存较小,停顿时间可以接受)。

4.2 ParNew收集器

类型

新生代收集器

就是多线程并行的Serial收集器版本。

应用场景

有可能会用到服务器端,因为可以和CMS配合着使用。

CMS收集老年代,ParNew收集新生代。

JDK9之后,ParNew就合并到CMS中了。

4.3 Parallel Scavenge收集器

类型

新生代收集器

也是多线程的,和ParNew类似

不同的是,别都是的关注缩小垃圾回收时的**“停顿时间”**

而它是关注如何达到一个可控制的**“吞吐量”**,拥有大局观

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

两个参数:

  • -XX:MaxGCPauseMillis :垃圾回收时间的阈值
  • -XX:GCTimeRatio : 吞吐量的倒数,默认99(1/(1+99)),即1%的垃圾回收时间。

4.4 Serial Old收集器

Serial收集器的老年代版本。

4.5 Parallel Old

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

基于标记—整理算法实现。

4.6 CMS收集器(重点)

Concurrent Mark Sweep

目标

获取最短回收停顿时间(Stop The World!)

过程

基于 标记—清除 算法

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

初始标记:只标记GC Roots直接关联的对象。(Stop The World!)

并发标记:从GC Roots直接关联的对象开始遍历整个对象图的过程。(耗时较长)

重新标记:标记上一步过程中,用户操作导致标记变动的那部分对象的标记记录。(Stop The World!)

并发清除:清除判死刑的对象。(耗时较长)

优点
  • 并发收集
  • 低停顿

缺点:

  • 对处理器资源非常敏感

    处理器核心数量<4时,CMS对用户程序的影响十分的大。

  • 无法处理浮动垃圾

  • 会产生大量空间碎片

4.7 G1收集器(Garbage First)

创新一:Region堆布局

传统的GC是把Java堆分为固定大小的新生代和老年代

G1:

先划分为多个大小相等的Region,再根据需要,赋予每个Region不同的角色——化整为零

创新二:目标范围

Mixed GC

不再已分代为基准收集(新生代、老年代、或整个Java堆)——每次都是大扫除。

而是看:哪块内存垃圾多,回收的收益最大——能应付新对象分配就行了,不图每次都是大扫除。

创新三:建立可预测的停顿时间模型

因为G1把Region作为垃圾回收的最小单位,所以,每次回收的内存都是Region的整数倍。

这样就能有计划的避免在整个Java堆中回收垃圾。—需要多少收多少,没必要全部收集

创新四:优先级列表(Garbage First名字的由来)

根据Region的“价值”建立优先级列表,每次优先处理回收价值大的Region。

价值:可回收内存的大小以及回收时间的经验值

创新五:可以指定收集停顿时间

不能太大,也不能太小

太大很好理解,停顿时间太长,用户体验变差

太小的话,每次只能回收一点垃圾,时间长了,垃圾堆的越来越多,出现Full GC,得不偿失

运作过程
  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

前三个和CMS类似

筛选回收:根据Region的优先级进行回收,采用的是“标记—复制”算法,把待回收Region中存货的对象,复制到空的Region中

因为要移动对象,所以要停顿**(Stop The World!)**

与CMS比较
不同:

收集算法不同:

G1:

整理看是:标记—整理(把存活的对象整理到空的Region区域)

局部看是:标记—复制(从一个Region复制到另外一个Region)

两者反正都不会产生空间碎片

CMS:

标记清除

G1优势:
  • 可指定最大停顿时间
  • Region的内存布局
  • 按收益动态确定回收集(优先级列表)
  • 将“行为”与“实现“进行分离
G1弱势:
  • 内存占用高
  • 额外执行负载高
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值