JVM系列4-垃圾收集器与内存分配策略(二)

简介: JVM系列4-垃圾收集器与内存分配策略(二)

JVM系列4-垃圾收集器与内存分配策略(一):https://developer.aliyun.com/article/1535563

Parallel Old

同Serial Old一样,Parallel Old是Parallel Scavenge的老年代版本。在注重吞吐量和CPU资源敏感的地方都可以优先考虑Parallel Old可以和Parallel Scavenge一起搭配使用。

CMS收集器

CMS(Concurrency Mark Sweep)是一个以获取最短回收停顿时间为目标的收集器,允许垃圾回收线程和用户工作线程同时运行。其使用“标记-清除”算法。目前来说例如淘宝等大型互联网企业都希望请求响应时间能尽量短,并且垃圾回收的停顿时间也尽量短,这种情况就可以使用CMS收集器。

CMS的“标记-清除”算法分为多个步骤:

  1. 初始标记:初始标记是用于标记直接与GC Roots关联的对象,不需要遍历下去,所需的时间很短。这一过程会发生STW(Stop the World)。
  2. 并发标记:并发标记就是遍历所有与GC Roots直接或者间接关联的对象。
  3. 重新标记:前面说过CMS允许垃圾回收线程和用户工作线程同时运行,所以这一过程是为了标记在前面标记过程中发生变动的对象。这一过程会发生STW(Stop the World)。
  4. 并发清除:清除掉那些没有被标记的对象。

其中并发标记和并发清除过程耗费时间最长,但是这两个阶段都可以并发进行,所以对用户的影响也不会太大。

虽然CMS确实是一款很不错的垃圾收集器,但是其也还有几个缺点:

  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  • CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。同时为了保证在垃圾回收的同时用户线程也可以正常工作,所以不可能对整个区域进行回收,需要预留一部分区域给用户线程,如果在垃圾回收阶段,预留的垃圾回收区域不足,就可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
  • CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

G1收集器

G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:

  • 并行与并发:G1收集器能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器。从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。

在G1中Heap被分成一块块大小相等的region,Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不指定,那么G1会根据Heap大小自动决定。保留新生代和老年代的概念,但它们不需要物理上的隔离。每块region都会被打唯一的分代标志(eden,survivor,old),代表一个分代类型的region可以是不连续的。eden regions构成Eden空间,survivor regions构成Survivor空间,old regions构成了old 空间。通过命令行参数-XX:NewRatio=n来配置新生代与老年代的比例,n为整数,默认为2,即比例为2:1;-XX:SurvivorRatio=n可以配置Eden与Survivor的比例,默认为8。

G1收集器进行回收大致可分为以下几个阶段:

  • 初始标记:同CMS功能基本一致初始标记是用于标记直接与GC Roots关联的对象,不需要遍历下去,所需的时间很短。这一过程会发生STW(Stop the World)。
  • 并发标记:并发标记就是遍历所有与GC Roots直接或者间接关联的对象。遍历整个堆寻找活跃对象,这个发生在应用运行时,这个阶段可以被年轻代垃圾回收打断。
  • 重新标记:这一过程是为了标记在前面标记过程中发生变动的对象,和CMS的重新标记过程功能上基本保持一致。但是G1使用一个叫作snapshot-at-the-beginning(SATB)的比CMS收集器的更快的算法。
  • 筛选回收:进行垃圾回收,G1保留了YGC并加上了一种全新的MIXGC用于收集老年代。G1中没有Full GC,G1中的Full GC是采用serial old的Full GC。
Young GC

当Eden空间不足时就会触发YGC。在G1中YGC也是采用复制存活对象到survivor空间,对于对象的存活年龄满足晋升条件时,把对象移到老年代。

在对新生代进行垃圾回收时,需要判断哪些对象能够会被回收。这里判断的方法也是采用可达性分析,标记与GC Roots直接或间接关联的对象。在CMS中使用了Card Table的结构,里面记录了老年代对象到新生代引用。G1也是使用这个思路,定义了一个新的数据结构:Remembered Set。在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。在进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

Full GC

full gc是指对包括新生代、老年代和方法区(元空间)等地区进行垃圾回收。

full gc的触发包括以下几种情况:

  • 老年代空间不足
  • 新生代对象晋升到老年代时,老年代剩余空间低于新生代晋升为老年代的速率,会触发老年代回收
  • minor gc之后,survior区内存不足,将存活对象放入老年代,老年代也不足,触发Full GC。本质上还是老年代内存不足。
  • System.gc().

理解GC日志

这里介绍一些打印出的gc日志的信息:

为了触发gc写一段代码,实际上也可以直接使用System.gc()

public class Test {

    public static void main(String[] args) {
        byte[] bytes1 = new byte[1024 * 1024];
        byte[] bytes2 = new byte[1024 * 1024];
        byte[] bytes3 = new byte[1024 * 1024];
        byte[] bytes4 = new byte[1024 * 1024];
        byte[] bytes5 = new byte[1024 * 1024];
    }

    public static void test(){
        test();
    }
}
复制代码

要在控制台打印gc信息需要我们手动的配一些参数:

  • -XX:+PrintGCDetails 输出GC的详细日志
  • -XX:+PrintGCTimeStamps/PrintGCDateStamps 输出GC的时间戳
  • -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
  • -Xloggc:../logs/gc.log 日志文件的输出路径

我这里使用Idea,直接在VM args配置即可:

现在运行上面的程序即可在控制台获得gc信息:

2019-01-24T20:08:25.811+0800: [GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.872+0800: [GC (Allocation Failure) [PSYoungGen: 1504K->488K(1536K)] 1624K->780K(5632K), 0.0016239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.879+0800: [GC (Allocation Failure) [PSYoungGen: 653K->504K(1536K)] 4017K->3940K(5632K), 0.0009844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.880+0800: [GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 3940K->3948K(5632K), 0.0006796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.881+0800: [Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 3444K->3832K(4096K)] 3948K->3832K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0076471 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2019-01-24T20:08:25.888+0800: [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 3832K->3832K(5632K), 0.0003390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.889+0800: [Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at Test.main(Test.java:17)
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 3832K->3814K(4096K)] 3832K->3814K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0065960 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 1536K, used 65K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe104d8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 4096K, used 3814K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 93% used [0x00000000ffa00000,0x00000000ffdb9a60,0x00000000ffe00000)
 Metaspace       used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 377K, capacity 388K, committed 512K, reserved 1048576K
复制代码

上面的gc信息取一条分析:

[GC/Full GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

最前面的GC/FullGC表示gc类型,GC表示新生代gc(Minor GC),Full GC表示对新生代和老年代一起收集。

[PSYoungGen: 1019K->488K(1536K)]这个表示GC前该内存区域已使用容量-->GC后该内存区域已使用容量,后面圆括号里面的1536K为该内存区域的总容量。

紧跟着后面的1019K->608K(5632K),表示GC前Java堆已使用容量->GC后Java堆已使用容量,后面圆括号里面的5632K为Java堆总容量。

[Times: user=0.00 sys=0.00, real=0.00 secs]分别表示用户消耗的CPU时间,内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间,CPU时间和墙钟时间的差别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时。因为这里是测试在几乎一开始就发生了gc,并且设置的堆栈容量都较小,所以看不出时间。

PSYoungGen和ParOldGen分别代表新生代和老年代所使用的垃圾收集器。PSYoungGen表示Parallel Scavenge收集器,ParOldGen表示Parallel Old。要查看当前jvm使用那种收集器可以使用-XX:+PrintCommandLineFlags,命令行下运行即可。

java -XX:PrintCommandLineFlags -version

-XX:InitialHeapSize=132485376 -XX:MaxHeapSize=2119766016 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
复制代码

其中的-XX:+UseParallelGC表示使用Parallel Scavenge+Serial Old的组合,但是上面是Parallel Scavenge+parallel old的组合,这是为什么???

GC中的参数

这里有一篇不错的文章总结gc中的参数,比较详细:GC

实战:内存分配与回收策略

对象内存的分配,一般是在堆上进行分配,但是随着JIT技术的发展,部分对象直接在栈上进行内存分配。

在前面的分代收集算法小节处,已经描述了jvm中的分代,将堆分为新生代和老年代。在描述内存分配前,我们先来了解下不同的GC类型:

  • Minor GC:当Eden区可分配的内存不足以创建对象时就会触发一次Minor GC,Minor GC发生在新生代,由于新生代中大多数对象都是使用过后就不需要所以Minor GC的触发非常频繁。在Minor GC中存活下来的对象会被移到Survivor中,如果Survivor内存不够就直接移动到老年代。
  • full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC)。或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。本内容来源于R大回答。

对象分配

在大多数情况下,对象的内存分配都优先在Eden中进行分配,当Eden区可分配的内存不足以创建对象时就会触发一次Minor GC。将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域。如Minor GC时survivor空间不够,对象提前进入老年代,老年代空间不够时就进行Full GC。大对象直接进入老年代,避免在Eden区和Survivor区之间产生大量的内存复制,虚拟机提供了一个-XX:PretureSizeThreshold参数,令大于这个值得对象直接进入老年代,但是该参数支队Serial和ParNew收集器有效。 此 外大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间。

这里大对象主要是指那种需要大量连续内存的java对象,比如大数组或者特别长的字符串等。

对象晋级

年龄阈值:虚拟机为每个对象定义了一个对象年龄(Age)计数器, 经第一次Minor GC后 仍然存活,被移动到Survivor空间中, 并将年龄设为1。以后对象在Survivor区中每熬 过一次Minor GC年龄就+1。 当增加到一定程度(默认 15),将会晋升到老年代(晋级的年龄可以通过-XX:MaxTenuringThreshold进行设置)。

提前晋升: 动态年龄判定,如果在Survivor空间中相同年龄所有对象大小的总和大 于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代,而无 须等到晋升年龄。

空间分配担保

在前面说垃圾收集算法时关于复制对象有说过可能会存在存活下来的对象无法被survivor容纳,这时就需要老年代容纳无法被survivor容纳的对象。而如果老年代也没有足够的空间来存放这些对象的话就会触发一次Full GC。

在发生Minor GC之前, 虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总 空间, 如果这个条件成立, 那这一次Minor GC可以确保是安全的。 如果不成立, 则虚拟机会先查看- XX: HandlePromotionFailure参数的设置值是否允许担保失败( Handle Promotion Failure) ; 如果允 许, 那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大 于, 将尝试进行一次Minor GC, 尽管这次Minor GC是有风险的; 如果小于, 或者-XX: HandlePromotionFailure设置不允许冒险, 那这时就要改为进行一次Full GC。

解释一下“冒险”是冒了什么风险: 前面提到过, 新生代使用复制收集算法, 但为了内存利用率, 只使用其中一个Survivor空间来作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活, 需要老年代进行分配担保, 把Survivor无 法容纳的对象直接送入老年代, 这与生活中贷款担保类似。 老年代要进行这样的担保, 前提是老年代 本身还有容纳这些对象的剩余空间, 但一共有多少对象会在这次回收中活下来在实际完成内存回收之 前是无法明确知道的, 所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值, 与 老年代的剩余空间进行比较, 决定是否进行Full GC来让老年代腾出更多空间。

取历史平均值来比较其实仍然是一种赌概率的解决办法, 也就是说假如某次Minor GC存活后的对 象突增, 远远高于历史平均值的话, 依然会导致担保失败。 如果出现了担保失败, 那就只好老老实实 地重新发起一次Full GC, 这样停顿时间就很长了。 虽然担保失败时绕的圈子是最大的, 但通常情况下 都还是会将-XX: HandlePromotionFailure开关打开, 避免Full GC过于频繁。 参见代码清单3-11, 请读 者先以JDK 6 Update 24之前的HotSpot运行测试代码。

目录
相关文章
|
2天前
|
算法 Java
Java垃圾回收(Garbage Collection,GC)是Java虚拟机(JVM)的一种自动内存管理机制,用于在运行时自动回收不再使用的对象所占的内存空间
【6月更文挑战第18天】Java的GC自动回收内存,包括标记清除(产生碎片)、复制(效率低)、标记整理(兼顾连续性与效率)和分代收集(区分新生代和老年代,用不同算法优化)等策略。现代JVM通常采用分代收集,以平衡性能和内存利用率。
26 3
|
7天前
|
存储 监控 算法
【JVM】如何定位、解决内存泄漏和溢出
【JVM】如何定位、解决内存泄漏和溢出
21 0
|
7天前
|
存储 监控 算法
JVM系列4-垃圾收集器与内存分配策略(一)
JVM系列4-垃圾收集器与内存分配策略(一)
17 0
|
4天前
|
消息中间件 存储 Kafka
实时计算 Flink版产品使用问题之 从Kafka读取数据,并与两个仅在任务启动时读取一次的维度表进行内连接(inner join)时,如果没有匹配到的数据会被直接丢弃还是会被存储在内存中
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
22小时前
|
存储 安全 Unix
【内网安全】Win&Linux&内存离线读取&Hashcat破解&RDP&SSH存储提取
【内网安全】Win&Linux&内存离线读取&Hashcat破解&RDP&SSH存储提取
|
5天前
|
存储 小程序 编译器
【C语言基础】:数据在内存中的存储
【C语言基础】:数据在内存中的存储
|
7天前
|
存储 C++
C primer plus 学习笔记 第12章 存储类别、链接和内存管理
C primer plus 学习笔记 第12章 存储类别、链接和内存管理
|
14天前
|
存储 编译器 C语言
C语言学习记录——数据的存储(数据类型、类型的基本归类、整型在内存中的存储、大小端介绍、浮点型在内存中的存储)二
C语言学习记录——数据的存储(数据类型、类型的基本归类、整型在内存中的存储、大小端介绍、浮点型在内存中的存储)二
13 0
|
14天前
|
存储 编译器 C语言
C语言学习记录——数据的存储(数据类型、类型的基本归类、整型在内存中的存储、大小端介绍、浮点型在内存中的存储)一
C语言学习记录——数据的存储(数据类型、类型的基本归类、整型在内存中的存储、大小端介绍、浮点型在内存中的存储)一
21 2
|
14天前
|
存储 缓存 NoSQL
了解Redis,第一弹,什么是RedisRedis主要适用于分布式系统,用来用缓存,存储数据,在内存中存储那么为什么说是分布式呢?什么叫分布式什么是单机架构微服务架构微服务的本质
了解Redis,第一弹,什么是RedisRedis主要适用于分布式系统,用来用缓存,存储数据,在内存中存储那么为什么说是分布式呢?什么叫分布式什么是单机架构微服务架构微服务的本质