垃圾回收

jvm 内存分配及垃圾回收

jvm 垃圾回收主要是针对jvm内存模型的jvm堆。通俗来讲,jvm垃圾回收主要就是针对堆内存的分配与回收。

jvm 堆是垃圾回收的主要区域,也叫做GC堆(garbage collected heap),从垃圾回收的角度,
由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:
新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。
进一步划分的目的是更好地回收内存,或者更快地分配内存。

堆空间的基本结构:

上图所示的 Eden 区、From Survivor0(“From”) 区、To Survivor1(“To”) 区都属于新生代,Old Memory 区属于老年代。
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,
则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),
当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,
可以通过参数 -XX:MaxTenuringThreshold 来设置。

####配置新生代 -----> 老年代jvm参数 -XX:MaxTenuringThreshold

注意:并不是设置了 MaxTenuringThreshold 年龄就一定会按照 MaxTenuringThreshold设置的年龄来迭代
因为这里还涉及到一个东西,那就是jvm动态规划,动态规划代码如下:

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
	//survivor_capacity是survivor空间的大小
  size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
  size_t total = 0;
  uint age = 1;
  while (age < table_size) {
    total += sizes[age];//sizes数组是每个年龄段对象大小
    if (total > desired_survivor_size) break;
    age++;
  }
  uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
	...
}

可能这里有些兄弟不太理解是什么意思,大概讲一下
eg:
如果我们设置的Survivor(SurvivorRatio) 是64M,那么默认的desired Survivor = 32M,此时Survivor 区age < 2
的对象累计大小为42M,那么 42 > 32,晋升年龄被设置为2,那么下次Minor GC时将年龄超过2的对象晋级到老年代。就会导致
old generation 快速填满,触发old gc,所以这个时候建议调整-XX:SurvivorRatio参数。

-XX:SurvivorRatio 解释这里会经常有人误人子弟,具体公式其实是

eden = (RY)/(R+1+1)
Y代表我们设置的Xmn:60M 这里的意思是新生代堆大小(单位可以是G或者M)(eden + s0 + s1)
R代表SurvivorRatio 的数字儿
eg:
我们设置的Xmn为60,那么eden = 8
60 / 10 = 48M
同理:
from = Y/(R+1+1)
To = Y/(R+1+1)
为什么要加两个1?
因为survivor 有两个区S0,S1,告一段落,继续讲动态规划。
这里贴一个图,我的虚拟机配置参数如下:
-Xmn60M -XX:SurvivorRatio=4 -XX:+PrintFlagsFinal
新生代堆分配图如下:

jvm 为什么要进行动态规划

1、如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件: a)MaxTenuringThreshold设置的过大,
原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,
一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,
这样对象老化的机制就失效了。 b)MaxTenuringThreshold设置的过小,
“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,
老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能。
2、相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,
都会导致对象的生命周期分布发生波动,那么固定的阈值设定,
因为无法动态适应变化,会造成和上面相同的问题。

对象优先在eden区分配

目前主流的垃圾回收器都采用分代回收算法,因此需要将堆内存分为新生代和老年代,
这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,
虚拟机将发起一次 Minor GC.下面我们来进行实际测试以下。

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2;
        allocation1 = new byte[30900*1024];
        //allocation2 = new byte[900*1024];
    }
}

GC 参数: -XX:+PrintGCDetails -Xmn60M
这里设置了一个新生代为60M,能够更好的反应对象创建后占eden的比例

如图: 当前我们新生代为53760K
这里有个疑问,为什么 PSYoungGen is not sum eden,from and to
简单来说:就是total = eden + from ;
Answer:
The total size reported for the Young Generation always includes the eden and from space only,
ignoring the always-empty to space, which is inconsistent to the addresses reported
behind the sizes, which include the complete span, covering all three spaces.


继续验证对象大多数时间都在eden区这个理论:
当我们集徐创建第二个大对象的时候:

我们发现eden 区的空战使用变少了:
原因: 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,
我们刚刚讲了当 Eden 区没有足够空间进行分配时,
虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,
所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,
老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,
后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。可以执行如下代码验证:

public class GCTest {
   public static void main(String[] args) {
       byte[] allocation1, allocation2,allocation3,allocation4;
       allocation1 = new byte[40080*1024];

       //allocation2 = new byte[1000*1024];
       //allocation3 = new byte[1000*1024];
       //allocation4 = new byte[1000*1024];
   }
} 
主要进行gc的区域

对象是否已经死亡?

引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
可达性分析算法
现代虚拟机基本都是采用这种算法来判断对象是否存活,可达性算法的原理是以一系列叫做
GC Root 的对象为起点出发,引出它们指向的下一个节点,
再以下个节点为起点,引出此节点指向的下一个结点。。。
(这样通过 GC Root 串成的一条线就叫引用链),
直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,
则这些对象会被判断为「垃圾」,会被 GC 回收。

如图示,如果用可达性算法即可解决上述循环引用的问题,
因为从GC Root 出发没有到达 a,b,所以 a,b 可回收
不可达的对象是否就会被回收?
并不是!可达性算法首先会对不可达的对象进行标记,紧接着会筛选此对象是否有必要执行finalize方法。
当对象没有覆盖finalize方法或者方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
如何判断一个常量是废弃常量
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,
就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc”
就会被系统清理出常量池了。

垃圾搜集算法

标记清除法
算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,
在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,
后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
1、效率问题
2、空间问题(标记清除后会产生大量不连续的碎片)

标记-复制算法
把堆等分成两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用以上所说的标记法把存活的对象标记出来(下图有误无需清除),然后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了。

缺点:
标记复制算法会浪费大量内存空间,虽然剩下的都是大量的连续空间,但是会导致浪费了一半的空间。

标记-整理算法
前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,
即将所有的存活对象都往一端移动,紧邻排列(如图示),
再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。

缺点:
每进一次垃圾清除都要频繁地移动存活的对象,效率低下

分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,
只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代
,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,
所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,
所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

新生代垃圾收集器

Serial 收集器

Serial 收集器是工作在新生代的,单线程的垃圾收集器,单线程意味着它只会使用一个 CPU 或一个收集线程来完成垃圾回收,不仅如此,还记得我们上文提到的 STW 了吗,它在进行垃圾收集时,
其他用户线程会暂停,直到垃圾收集结束,也就是说在 GC 期间,此时的应用不可用。

看起来单线程垃圾收集器不太实用,不过我们需要知道的任何技术的使用都不能脱离场景,
在 Client 模式下,它简单有效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,
Serial 单线程模式无需与其他线程交互,减少了开销,专心做 GC 能将其单线程的优势发挥到极致,
另外在用户的桌面应用场景,分配给虚拟机的内存一般不会很大,
收集几十甚至一两百兆(仅是新生代的内存,桌面应用基本不会再大了),
STW 时间可以控制在一百多毫秒内,只要不是频繁发生,这点停顿是可以接受的,
所以对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器

ParNew 收集器
ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程,
其他像收集算法,STW,对象分配规则,回收策略与 Serial 收集器完成一样,
在底层上,这两种收集器也共用了相当多的代码,它的垃圾收集过程如下:

ParNew 主要工作在 Server 模式,我们知道服务端如果接收的请求多了,
响应时间就很重要了,多线程可以让垃圾回收得更快,也就是减少了 STW 时间,
能提升响应时间,所以是许多运行在 Server 模式下的虚拟机的首选新生代收集器,
另一个与性能无关的原因是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作,
CMS 是一个划时代的垃圾收集器,是真正意义上的并发收集器,
它第一次实现了垃圾收集线程与用户线程(基本上)同时工作,它采用的是传统的 GC 收集器代码框架,
与 Serial,ParNew 共用一套代码框架,所以能与这两者一起配合工作,
而后文提到的 Parallel Scavenge 与 G1 收集器没有使用传统的 GC 收集器代码框架
,而是另起炉灶独立实现的,另外一些收集器则只是共用了部分的框架代码,
所以无法与 CMS 收集器一起配合工作。

在多 CPU 的情况下,由于 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快
,也能有效地减少 STW 的时间,提升应用的响应速度。

Parallel Scavenge 收集器
Parallel Scavenge 收集器也是一个使用复制算法,多线程,
工作于新生代的垃圾收集器,看起来功能和 ParNew 收集器一样。

CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,
而 Parallel Scavenge 目标是达到一个可控制的吞吐量
(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),
也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短
,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,
所以更适合做后台运算等不需要太多用户交互的任务。

Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量,
分别是控制最大垃圾收集时间的 -XX:MaxGCPauseMillis
参数及直接设置吞吐量大小的 -XX:GCTimeRatio(默认99%)

除了以上两个参数,还可以用 Parallel Scavenge 收集器提供的第三个参数
-XX:UseAdaptiveSizePolicy,开启这个参数后,就不需要手工指定新生代大小,
Eden 与 Survivor 比例(SurvivorRatio)等细节,只需要设置好基本的堆大小
(-Xmx 设置最大堆),以及最大垃圾收集时间与吞吐量大小,
虚拟机就会根据当前系统运行情况收集监控信息,
动态调整这些参数以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标。
自适应策略也是 Parallel Scavenge 与 ParNew 的重要区别!

parallel Scavenge 收集器 jvm参数意义:

 -XX:+UseParallelGC
 
     使用 Parallel 收集器+ 老年代串行
 
 -XX:+UseParallelOldGC
 
     使用 Parallel 收集器+ 老年代并行

这是jdk8使用的默认收集器!!!
使用 java -XX:+PrintCommandLineFlags -version 命令查看
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,
则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能

老年代垃圾回收器

Serial Old 收集器
Serial 收集器是工作于新生代的单线程收集器,
与之相对地,Serial Old 是工作于老年代的单线程收集器,
此收集器的主要意义在于给 Client 模式下的虚拟机使用,
如果在 Server 模式下,则它还有两大用途:
一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用,
另一种是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用(后文讲述),
它与 Serial 收集器配合使用示意图如下:

Parallel Old 收集器
Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,
两者组合示意图如下,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标

CMS 收集器
CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是个很不错的选择!

我们之前说老年代主要用标记整理法,而 CMS 虽然工作于老年代,但采用的是标记清除法,主要有以下四个步骤
1、初始标记 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
2、并发标记 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
3、重新标记 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
4、并发清除 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

优点:
从图中可以的看到初始标记和重新标记两个阶段会发生 STW,造成用户线程挂起,
不过初始标记仅标记 GC Roots 能关联的对象,速度很快,并发标记是进行 GC Roots Tracing 的过程,
重新标记是为了修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,
这一阶段停顿时间一般比初始标记阶段稍长,但远比并发标记时间短。

整个过程中耗时最长的是并发标记和标记清理,不过这两个阶段用户线程都可工作,
所以不影响应用的正常使用,所以总体上看,可以认为 CMS 收集器的内存回收过程是与用户线程
一起并发执行的。

缺点:
1、CMS 收集器对 CPU 资源非常敏感 原因也可以理解,
比如本来我本来可以有 10 个用户线程处理请求,
现在却要分出 3 个作为回收线程,吞吐量下降了30%,
CMS 默认启动的回收线程数是 (CPU数量+3)/ 4, 如果 CPU 数量只有一两个,
那吞吐量就直接下降 50%,显然是不可接受的

2、CMS 无法处理浮动垃圾(Floating Garbage),可能出现 「Concurrent Mode Failure」而导致另一次
Full GC 的产生,由于在并发清理阶段用户线程还在运行,所以清理的同时新的垃圾也在不断出现,
这部分垃圾只能在下一次 GC 时再清理掉(即浮云垃圾),同时在垃圾收集阶段用户线程也要继续运行,
就需要预留足够多的空间要确保用户线程正常执行,
这就意味着 CMS 收集器不能像其他收集器一样等老年代满了再使用,
JDK 1.5 默认当老年代使用了68%空间后就会被激活,当然这个比例可以通过
-XX:CMSInitiatingOccupancyFraction 来设置,但是如果设置地太高很容易导致在 CMS
运行期间预留的内存无法满足程序要求,会导致 Concurrent Mode Failure 失败,
这时会启用 Serial Old 收集器来重新进行老年代的收集,而我们知道 Serial Old
收集器是单线程收集器,这样就会导致 STW 更长了。

3、CMS 采用的是标记清除法,上文我们已经提到这种方法会产生大量的内存碎片,
这样会给大内存分配带来很大的麻烦,如果无法找到足够大的连续空间来分配对象,
将会触发 Full GC,这会影响应用的性能。当然我们可以开启 -XX:+UseCMSCompactAtFullCollection
(默认是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,
内存整理会导致 STW,停顿时间会变长,还可以用另一个参数 -XX:CMSFullGCsBeforeCompation
用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的。

G1收集器(grabage first)
G1 收集器是面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器,主要有以下几个特点
1、像 CMS 收集器一样,能与应用程序线程并发执行G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
2、整理空闲空间更快。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
3、需要 GC 停顿时间更好预测。与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
4、不会像 CMS 那样牺牲大量的吞吐性能。这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
5、不需要更大的 Java Heap

G1收集器大致分为以下几个步骤:
初始标记
并发标记
最终标记
筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的
Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region
划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率
(把内存化整为零)。

ZGC 收集器
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。

在 ZGC 中出现 Stop The World 的情况会更少!

参考:
https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6
https://mp.weixin.qq.com/s/_AKQs-xXDHlk84HbwKUzOw
https://stackoverflow.com/questions/55175723/psyounggen-is-not-the-sum-of-eden-from-and-to
https://blog.csdn.net/flyfhj/article/details/86630105
原文: http://www.clhardstone.cn/2021/05/10/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/#more

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值