JVM堆内存

JAVA堆内存管理是影响性能主要因素之一,堆内存溢出是Java运行时常见的故障,首先看下JAVA堆内存工作划分,如图:

JVM内存划分为堆内存和非堆内存,堆内存又分为年轻代、老年代、非堆内存就只有永久代(在JDK1.8之后已废弃,改为元空间)

年轻代又分为Eden和Survivor两个区,Survivor则是有FromSpace和ToSpace组成,Eden区占比较大的容量,Survivor两个区占用容量较小,默认比例为8:1:1

堆内存的用途:主要存放的是对象实例,当Java创建了一个类的实例对象或者数组的时候,会在堆中为新的对象实例分配内存,虚拟机中只有一个堆,程序中的所有线程都共享它,堆的存取方式为管道类型,先进先出,在程序运行时,可自定义设置堆的大小

非堆内存用途:元空间,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等

注意:元空间并不在JVM中,而是使用本地的内存

元空间有注意有两个参数:

  • MetaspaceSize :初始化元空间大小,控制发生GC阈值
  • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存,默认无限制

GC收集顺序如下:

  • 当Java创建一个类的实例后,首先将此实例对象放在年轻代Eden区,Eden区空间满了后会触发Minor GC,将存活的对象通过复制算法复制到Suvivor0区,也就是上图中的FromSpace
  • 当Suvivor0区满了后,再次触发MinorGC,将Suvivor0中的存活对象复制到Suvivor1区,也就是上面的ToSpace区
  • 每经过一次Major GC仍存活,年龄就加1,当对象达到一定年龄时(默认15岁),就会被移到老年代
  • 老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

Minor GC : 清理年轻代

Major GC : 清理老年代

Full GC : 清理整个堆空间,包括年轻代、老年代和永久代,所有GC都会停止应用所有线程

FromSpace和ToSpace的主要区别:

  •  FromSpace – 这个空间用于最初分配对象。当它即将满时,Minor GC 就会启动
  • ToSpace – 这个空间最初是空的。Minor GC 时,存活的对象会被复制到这里
  •  每次 Minor GC 后,fromSpace 和 toSpace 的角色会交换。新的对象会分配到原来的 toSpace 中,这个空间现在变成了 fromSpace
  • fromSpace 和 toSpace 的大小是相等的,以容纳存活对象的复制操作
  • Minor GC 使用复制算法,就是将 fromSpace 中存活的对象复制到 toSpace,然后清理 fromSpace

JVM堆内存常用参数

参数描述
-Xms堆内存初始大小,单位m、g
-Xmx(MaxHeapSize)堆内存最大允许大小,一般不要大于物理内存的80%
-XX:NewSize(-Xns)年轻代内存初始大小
-XX:MaxNewSize(-Xmn)年轻代内存最大允许大小,官方推荐配置为整个堆的3/8
-XX:PermSize非堆内存初始大小,一般设置初始化200m,最大1024m足够
-XX:MaxPermSize非堆内存最大允许大小
-XX:MetaspaceSize设置元空间最小大小(JDK8以后)
-XX:MaxMetaspaceSize设置元空间最大大小(JDK8以后)
-Xss设置每个线程的堆栈大小,JDK5以后默认为1M

什么情况下会出现堆内存溢出?

年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)

OOM常见有以下几个原因:

  • 老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
  • 永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
  • 代码bug,占用内存无法及时回收

为什么要分多种代?

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短

为什么Survivor分为两块大小相等的幸存空间?

主要为了解决碎片化。如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC

垃圾回收算法

1、标记-清除(Mark-Sweep)

GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC

2、复制(Copy)

将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间

3、标记-整理(Mark-Compact)

也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题

一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收

垃圾收集器

1、串行收集器(Serial)

比较老的收集器,单线程。收集时,必须暂停应用的工作线程,直到收集结束

2、并行收集器(Parallel)

多条垃圾收集线程并行工作,在多核CPU下效率更高,应用线程仍然处于等待状态

3、CMS收集器(Concurrent Mark Sweep)

CMS收集器是缩短暂停应用时间为目标而设计的,是基于标记-清除算法实现,整个过程分为4个步骤,包括:

  • 初始标记(Initial Mark)
  • 并发标记(Concurrent Mark)
  • 重新标记(Remark)
  • 并发清除(Concurrent Sweep)

初始标记、重新标记这两个步骤仍然需要暂停应用线程。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段是标记可回收对象,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,这个阶段暂停时间比初始标记阶段稍长一点,但远比并发标记时间段,由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,CMS收集器内存回收与用户一起并发执行的,大大减少了暂停时间

4、G1收集器(Garbage First)

G1收集器将堆内存划分多个大小相等的独立区域(Region),并且能预测暂停时间,能预测原因它能避免对整个堆进行全区收集。G1跟踪各个Region里的垃圾堆积价值大小(所获得空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证了再有限时间内获得更高的收集效率

G1收集器工作工程分为4个步骤,包括:

  • 初始标记(Initial Mark)
  • 并发标记(Concurrent Mark)
  • 最终标记(Final Mark)
  • 筛选回收(Live Data Counting and Evacuation)

初始标记与CMS一样,标记一下GC Roots能直接关联到的对象。并发标记从GC Root开始标记存活对象,这个阶段耗时比较长,但也可以与应用线程并发执行。而最终标记也是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录。最后在筛选回收阶段对各个Region回收价值和成本进行排序,根据用户所期望的GC暂停时间来执行回收

垃圾收集器参数

参数描述
-XX:+UseSerialGC串行收集器
-XX:+UseParallelGC并行收集器
-XX:+UseParallelGCThreads=8并行收集器线程数,同时有多少个线程进行垃圾回收,一般与CPU数量相等
-XX:+UseParallelOldGC指定老年代为并行收集
-XX:+UseConcMarkSweepGCCMS收集器(并发收集器)
-XX:+UseCMSCompactAtFullCollection开启内存空间压缩和整理,防止过多内存碎片
-XX:CMSFullGCsBeforeCompaction=0表示多少次Full GC后开始压缩和整理,0表示每次Full GC后立即执行压缩和整理
-XX:CMSInitiatingOccupancyFraction=80%表示老年代内存空间使用80%时开始执行CMS收集,防止过多的Full GC
-XX:+UseG1GCG1收集器
-XX:MaxTenuringThreshold=0在年轻代经过几次GC后还存活,就进入老年代,0表示直接进入老年代

配置示例

1、配置年轻代使用并行收集器,如下:

java -Xmx2048m -Xms2048m -Xmn2g -Xss256k -XX:+UseParallelGC -XX:ParallelGCThreads=8

上述配置的并行收集器仅对年轻代有效,老年代仍然使用串行收集器

2、添加一个参数,让老年代也是用并行收集器,如下:

java -Xmx2048m -Xms2048m -Xmn2g -Xss256k -XX:+UseParallelGC -XX:ParallelGCThreads=8 -XX:+UseParallelOldGC

常见配置如下:

1、堆配置

-Xms:初始堆大小 
-Xmx:最大堆大小 
-XX:NewSize=n:设置年轻代大小 
-XX:NewRatio=n:设置年轻代和年老代的比值.如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值.注意Survivor区有两个.如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 
-XX:MaxPermSize=n:设置持久代大小

2、收集器设置 

-XX:+UseSerialGC:设置串行收集器 
-XX:+UseParallelGC:设置并行收集器 
-XX:+UseParalledlOldGC:设置并行年老代收集器 
-XX:+UseConcMarkSweepGC:设置并发收集器

3、垃圾回收统计信息

-XX:+PrintGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:filename

4、并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数.并行收集线程数. 
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比.公式为1/(1+n)

JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4.默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时, JVM会减少堆直到-Xms的最小限制.因此一般设置-Xms,-Xmx相等以避免在每次GC 后调整堆的大小

常用命令

1、查看堆内存信息

jmap -heap pid      #查看堆内存

2、打印堆栈信息

jmap -dump:format=b,file=java.dump PID

3、在启动java程序的时候指定参数来打印dump

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm.dump

4、生产环境可使用arthas来排查问题

首先执行命令jar -jar arthas-boot.jar,此时会列出机器上全部的Java进程,如图:

输入前面的序号,进入到进程里面,如图:

输入dashboard后,进入全局页面,可查看线程,内存,GC,环境等信息,截取堆内存部分如图:

输入thread可以查看线程详细情况,如图:

输入 thread加上线程ID 可以查看线程堆栈,如图:

5、配置输出GC日志

java -jar -Xloggc:/data/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps  -XX:+PrintGCTimeStamps -XX:+PrintGCCause  -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M    abc.jar
  • -XX:+PrintGCDetails:打印每次GC的详细日志
  • -XX:+PrintGCDateStamps:在GC日志前打印当前日期
  • -XX:+PrintGCTimeStamps:在GC日志前打印当前时间
  • -XX:+PrintGCCause:打印触发GC的原因
  • -XX:+UseGCLogFileRotation:启用GC日志文件的自动转储
  • -XX:NumberOfGCLogFiles=10:GC日志文件循环数目为10
  • -XX:GCLogFileSize=100M:每个GC日志文件的大小为100MB

标签