JAVA系列-GC

1. OOM

1) 常见OOM异常类型

(1) StackOverFlowError

栈内存溢出,用于深度方法调用(循环递归);

(2) OutOfMemoryError:Java heap space

用于变量申请的空间大于jvm的最大值;

(3) OutOfMemoryError: GC overhead limit exceed

GC回收的时间过长时会抛出OutOfMemoryError,过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead limit 错误会发生什么情况?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果.

(4) OutOfMemoryError: Direct buffer memory

堆外内存溢出,主要呈现在写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

(5) OutOfMemoryError: unable to create new native thread

你的应用创建了太多线程了,一个应用进程创建了多个线程,超过系统承载极限,你的服务器并不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报此错误;

  • linux查看当前用户的线程创建数量(被允许):ulimit -u; 
  • 修改当前用户的被允许的线程创建数量:.vim /etc/security/limits.d/90-nproc.conf

(6) OutOfMemoryError: Metaspace

元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存的限制。

 

2. 垃圾回收算法

1) 垃圾回收算法

GC算法是内存回收的方法论,常用的垃圾回收算法:

  • 引用计数法:新生代
  • 复制拷贝算法(Copying):新生代
  • 标计清除(Mark-Sweep):老年代
  • 标计压缩/整理(Mark-Compact):老年代

2) 垃圾回收器

a. 说明:

针对垃圾回收算法的具体实现,目前为止,还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集

 

b. 4种主要的垃圾收集器

  • Serial: 串行垃圾收集器(-XX:+UseSerialGC) - 它为单线程环境设计且只使用一个线程进行垃圾回收会暂停所有的用户线程。所以不适合服务器环境;
  • Parallel: 并行垃圾收集器(-XX:+UseParallelGC) 多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景;
  • CMS: 并发垃圾收集器(-XX:+UseCMSGC)  用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用对响应时间有要求的场景;
  • G1: G1垃圾回收器(-XX:+UseG1GC) - 将堆内存分割成不同的区域然后并发的对其进行垃圾回收

  

垃圾回收器扩展知识:

c. 7种垃圾回收器

查看默认的垃圾回收器
  • JVM参数:java -XX:+PrintCommandLineFlags -version
-XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
java version "13.0.2" 2020-01-14
Java(TM) SE Runtime Environment (build 13.0.2+8)
Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)
Java的gc回收的类型主要有几种:
  • UseSerialGC: 串行新生代GC(复制拷贝)
  • UseSerialOldGC: 串行老年代GC(已废弃)
  • UseParallelGC: 并行新生代GC(复制算法)
  • UseConcMarkSweepGC: 并发老年代GC(标记清除)
  • UseParNewGC : 并行新生区GC
  • UseParallelOldGC :并行老生区GC
  • UseG1GC : G1GC覆盖新生代和老年代
7种垃圾回收器针对的回收区域:

 

GC回收器的脑图说明

查看是否用到的某个垃圾回收器
//查看当前java进程
# jps -l
30514 jdk.jcmd/sun.tools.jps.Jps
27507 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
27625 org.jetbrains.jps.cmdline.Launcher
 
//查看进程的某个jvm属性
#jinfo -flags 27625
VM Flags:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=134217728 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=734003200 -XX:MaxNewSize=440401920 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5836300 -XX:NonProfiledCodeHeapSize=122910970 -XX:ProfiledCodeHeapSize=122910970 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC
#jinfo -flag UseG1GC 27625
-XX:+UseG1GC
#jinfo -flag UseSerialGC 27625
-XX:-UseSerialGC

(3) 垃圾收集器参数配置

预设参数说明:
  • DefNew => Default New Generation
  • Tenured => Old
  • ParNew =>  Parallel New Generation
  • PSYoungGen => Parallel Scavenge
  • ParOldGen => Parallel Old Generation
JVM Server/Client模式: 当前基本64位系统使用的是JVM Server模式

A. 新生代回收器

a) 串行回收器:Serial收集器

一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。串行收集器是最古老,最稳定定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾回收过程中可能会产生较长的停顿(Stop-The-World状态)。虽然在收集垃圾过程中需要暂停所有的其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依赖是JVM运行在Client模式下默认的新生代垃圾收集器。

 

JVM参数说明:
  • 参数:-XX:+UseSerialGC
  • 效果: Serial(Young区用) + Serial Old(Old区用)的收集器组合,新生代、老年化都会使用串行回收收集器
  • 算法:新生代使用复制算法,老年代使用标记-整理算法;
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UserSerialGC
b) 并行回收器:ParNew

使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有的工作线程直到它收集结束。ParNew收集器其实就是Serial收集器新生代的并行多程程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多JVM运行在Server模式下新生代的默认垃圾收集器。

 

JVM参数说明:
  • 参数:-XX:+UseParNewGC;启用ParNew收集器只影响新生代的收集,不影响老年代。
  • 效果:会使用ParNew(Young区用) + Serial Old的收集器组合(java8后,serial Old已不推荐使用);
  • 算法:新生代使用复制算法,老年代采用标记-整理算法
备注:-XX:ParallelGCThreads 限制GC线程数量,默认开启和CPU数日相同的线程数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
c) 并行回收器:Parallel/Parallel Scavenge

Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化;

此收集器关注的重点:

  • 可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于后台运算而不需要太多交互的任务。
  • 自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:+MaxGCPauseMillis)或最大的吞吐量)。

JVM参数说明:
  • 参数:-XX:+UseParallelGC/-XX:+UseParallelOldGC(可以互相激活),启用Parallel Scanvenge收集器
  • 算法:新生代使用复制算法,老年代使用标记-整理算法
说明:
  • -XX:ParallelGCThreads=数字N :表示启动多个少GC线程,当cpu>8 N=5/8; 当cpu<8时,N=实际个数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC

B. 老年代回收器

a) 并行收集器:Parallel Old收集器

其是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。

  • JDK1.6之前,新生代:ParallelScavenge收集器 + 老年代Serial Old收集器,此组合只能保证新生代的吞吐量优先,无法保证整体的吞吐量。Parallel Old是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,
  • JDK1.8后可以优先考虑新生代:Parallel Scavenge+年老代:Parallel Old收集器的搭配策略。

JVM参数说明:
  • -XX:+UseParallelOldGC :启用Parallel Old收集器,设置此参数后,新生代Parallel Scavenge + 老年代Parallel Old同时启用;
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
b) 并发标记清除回收器:CMS(Concurrent Mark Sweep)

是一种以获取最短回收停顿时间为目标的收集器,适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器

Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行

cms回收过程:

个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark):只是标记一下GC Roots能直接关联的对象,速度很愉,仍然需要暂停所有工作线程;
  • 并发标记(CMS concurrent mark):进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象;
  • 重新标记(CMS remark):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程(由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正);
  • 并发清除(CMS concurrent sweep):清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记的结果,直接清理对象;

 

由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。

优缺点:

  1. 优点: 并发收集,低停顿
  2. 缺点:
    • 并发执行,对CPU资源压力大:由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,也就是说CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。
    • 采用的标记清除算法会导致大量碎片:标记清除算法无法整理空间碎片,老年代空间会随着应用时长被耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集后,进行一次压缩的Full GC。
JVM参数说明:
  • 参数:-XX:+UseConcMarkSweepGC 开启该参数后会自动将-XX:+UseParNewGC打开
  • 效果:开启上述参数后会使用ParNew(Young区用) + CMS(Old区用) + Serial Old的收集器组合, Serial Old将作为CMS出错后的备收集器
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
c) 串行标记清除回收器:Serial Old

Serial Old是Serial垃圾收集器的老年代版本,它同样是个单线程收集器,使用标记-整理算法,这个收集器也主要是运行在Client的JVM上的默认老年代垃圾收集器

它在Server模式下,主要有两上用途:

  • JDK1.5前版本中与新生代的Parallel Scavenge收集器搭配使用。(Parallel Scavenge + Serial Old)
  • 作为老年代中使用CMS收集器的后备垃圾收集方案;
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC

C. G1回收器

以前收集器的特点
  1. 年轻代和老年代是各自独立且连续的内存块;
  2. 年轻代收集使用单eden + s0 + s1进行复制算法;
  3. 老年代收集必须扫描整个老年代区域;
  4. 都是以尽可能少而快速地执行GC为设计原则;
G1的特点(JDK9中将G1变成默认的垃圾收集器以替代CMS)

G1(Garbage First)是一种服务端的垃圾收集器,应用在多处理器和大容量内存环境中,极少的减少垃圾收集的停顿时间,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。主要改变的是Eden, Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

具有以下特点:

  • 像CMS收集器一样,能与应用程序线程并发执行;
  • 整理空闲空间更快;
  • 需要更多的时间来预测GC停顿时间;
  • 不希望牺牲大量的吞吐性能;
  • 不需要更大的Java Heap;

G1收集器的设计目标是取代CMS收集器,它同CMS相比,有如下出色表现:

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多的能存碎片;
  • G1的Stop The World(STV)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间;

G1的整体特性:

  • G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW;
  • G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片;
  • 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘;
  • G1收集器里面讲整修模的内存区都混合在一起了,但期本身依然在小范围内要进行年轻代和老年代的共分,保留了年轻代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域;
  • G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备,G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
G1底层原理

Region区域化垃圾收集器:最大的好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB-32MB且必须是2的幂),默认将整堆划分为2048个分区。(大小范围在1MB-32MB,最多能设置2048个区域,即能够支持的最大内存为:32M * 2048 = 64GB)

  

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器:

  • 这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间;
  • 这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了;
  • 在G1中还有一种特殊的区域叫Humongous(巨大的)区域,如果一个对象占用的空间超过了分区容量的50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用于专门存放巨型对旬。如果一个H构装不下一个巨型对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC.
G1的回收步骤

针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内存碎片

  • Eden区的数据移动到Survivor区,假如出现Survivor空间不够,Eden区数据部分会晋升到Old区;
  • Survivor区的数据移动到新的Survivor区,部分数据会晋升到Old区;
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行;

 

 

 

G1四步过程
  • 初始标记:只标记GC Roots能直接关联到的对象;
  • 并发标记:进行GC Roots Tracing的过程;
  • 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象;
  • 筛选回收:根据时间来进行价值最大化的回收;

 

 

G1参数说明
  • -XX:+UseG1GC:开启G1回收
  • -XX:G1HeapRegionSize=n: 设置的G1中Region的大小,值是2的幂,范围是1MB~32MB。目标是根据最小的Java堆大小划分出约2048个区域;
  • -XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间;
  • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC, 默认为45%;
  • -XX:ConcGCThreads=n:并发GC使用的线程数;
  • -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值10%;
G1与CMS的对比优势
  • G1不会产生内存碎片;
  • 可以精确的控制GC停顿。该收集器是把整个堆(新生代、老生代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域;

(4) 如何选择垃圾收集器

A. 组合的选择:

  1. 单CPU或小内存,单机程序:-XX:+UseSerialGC
  2. 多CPU,需要最大吞吐量,如后台计算型应用:-XX:+UseParallelGC/+UseParallelOldGC
  3. 多CPU,追求低停顿时间,需快速响应如互联网应用:-XX:UseConcMarkSweepGC 和 -XX:+UseParNewGC

B. 整理表格

 

posted @ 2020-03-26 21:55  HappyDeveloper  阅读(363)  评论(0编辑  收藏  举报