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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 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运行测试代码。

目录
相关文章
|
3天前
|
Java Windows
为什么JVM在内存返还策略上会左右为难
为什么JVM在内存返还策略上会左右为难?
|
7天前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优的实用策略
在Java应用开发中,性能优化常常成为提升系统响应速度和处理能力的关键。本文将探讨Java虚拟机(JVM)调优的核心概念,包括垃圾回收、内存管理和编译器优化等方面,并提供一系列经过验证的调优技巧。通过这些实践指导,开发人员可以有效减少延迟,提高吞吐量,确保应用稳定运行。 【7月更文挑战第16天】
|
3天前
|
Java 编译器 C++
HotSpot JVM中的分层编译策略是怎样的
HotSpot JVM中的分层编译策略是怎样的?
|
5天前
|
存储 缓存 算法
操作系统中的内存管理优化策略
随着计算机技术的飞速发展,操作系统作为连接硬件与软件的桥梁,其性能的优劣直接影响着整个计算机系统的运行效率。在众多影响系统性能的因素中,内存管理无疑是至关重要的一环。本文将深入探讨现代操作系统中内存管理的优化策略,包括分页机制、虚拟内存技术、缓存策略等,旨在揭示这些技术如何提升系统性能并减少资源浪费。通过分析不同内存管理技术的优势与局限,本文为读者提供了对操作系统内存管理深度理解的同时,也指出了未来可能的发展方向。
9 0
|
7天前
|
存储 监控 算法
探索Java虚拟机:深入理解JVM内存模型和垃圾回收机制
在Java的世界中,JVM是核心所在,它不仅承载着代码的运行,还管理着内存资源。本文将带你深入了解JVM的内存模型和垃圾回收机制,通过具体数据与案例分析,揭示它们对Java应用性能的影响,并探讨如何优化JVM配置以提升效率。
|
9天前
|
监控 算法 Java
怎么用JDK自带工具进行JVM内存分析
JVM内存分析工具,如`jps`、`jcmd`、`jstat`、`jstack`和`jmap`,是诊断和优化Java应用的关键工具。`jps`列出Java进程,`jcmd`执行诊断任务,如查看JVM参数和线程堆栈,`jstat`监控内存和GC,`jstack`生成线程堆栈信息,而`jmap`则用于生成堆转储文件。这些工具帮助排查内存泄漏、优化内存配置、性能调优和异常分析。例如,`jmap -dump:file=heapdump.hprof <PID>`生成堆转储文件,之后可以用Eclipse Memory Analyzer (MAT)等工具分析。
|
12天前
|
存储 算法 Java
JAVA内存模型与JVM内存模型的区别
JAVA内存模型与JVM内存模型的区别
|
14天前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
20 0
|
14天前
|
存储 算法 安全
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
23 0
|
26天前
|
缓存 Java
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
23 0