JVM层的GC调优是生产环境上必不可少的一个环节,因为我们需要确定这个进程可以占用多少内存,以及设定一些参数的阀值。以此来优化项目的性能和提高可用性,而且这也是在面试中经常会被问到的问题。
想要进行GC调优,我们首先需要简单了解下JVM的内存结构,Java虚拟机的规范文档如下:
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
在介绍JVM内存结构之前,我们需要先知道运行时数据区这样的一个东西,它与JVM的内存结构有着一定的关联。不过它属于是一个规范,所以与JVM内存结构是有着物理上的区别的。运行时数据区如下:
1.程序计数器(Program Count Register,简称PC Register):
2.虚拟机栈(JVM Stacks):
3.堆Heap:
4.方法区(Method Area):
5.运行时常量池(Run-Time Constant Pool):
6.本地方法栈(Native Method Stacks):
了解了运行时方法区规范后,我们接下来看看JVM的内存结构图:
如上图,可以看到JVM内存被分为了两大区,非堆区用于存储对象以外的数据:
而堆区则用于存储对象相关数据:
在图中也可以看到,堆区还被分为了年轻代(young)和老年代(old)。那么为什么会有年轻代:
我们先来捋一捋,为什么需要把堆区分代?不分代不能完成它所做的事情么?其实不分代也完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都会存在同一个空间里。当进行GC的时候,我们就要找到哪些对象是没有用的,这样一来就需要对整个堆区进行扫描。而我们的很多对象都是只存活一瞬间的,所以GC就会比较频繁,而每次GC都得扫描整个堆区,就会导致性能低下。不进行GC的话,又会导致内存空间很快被占满。
因为GC性能的原因,所以我们才需要对堆区进行分代。如果进行分代的话,我们就可以把新创建的对象专门存放到一个单独的区域中,当进行GC的时候就优先把这块存放“短命”对象的区域进行回收,这样就会腾出很大的空间出来,并且由于不用去扫描整个堆区,也能极大提高GC的性能。
年轻代中的GC:
从上图中也可以看到年轻代被分为了三部分:1个Eden区和2个Survivor区,一般我们都会简称为S0、S1(同时它们还分为from和to两种角色),默认比例为8:1。一般情况下,最新创建的对象都会被分配到Eden区(一些大对象会特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
因为年轻代中的对象基本都是"短命"的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。所以才会有S0和S1区,复制算法的优点就是吞吐量高、可实现高速分配并且不会产生内存碎片,所以才适用于作为年轻代的GC算法。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
JVM中的对象分配:
我们了解完JVM内存结构后,再来看看一些常用的JVM参数:
1.设置年轻代的大小,和年轻代的最大值,具体的值需要根据实际业务场景进行判断。如果存在大量临时对象就可以设置大一些,否则小一些,一般为整个堆大小的1/3或者1/4。为了防止年轻代的堆收缩,两个参数的值需设为一样大:
2.设置Metaspace的大小,和Metaspace的最大值,同样需设为一样大:
3.设置Eden和其中一个Survivor的比例,这个值也比较重要:
4.设置young和old区的比例:
5.这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小:
6.用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1:
7.使用短直针,也就是启用压缩类空间(CCS):
8.设置CCS空间的大小,默认是一个G:
9.设置CodeCache的一个初始大小:
10.设置CodeCache的最大值:
11.设置多大的对象会被直接放进老年代:
12.长期存活的对象会被放入Old区,使用以下参数设置就可以设置对象的最大存活年龄:
注:如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论,linux64的java6默认值是15:
13.设置Young区每发生GC的时候,就打印有效的对象的岁数情况:
14.设置Survivor区发生GC后对象所存活的比例值:
本小节我们来简单介绍一些常见的垃圾回收算法,众所周知Java区别与C/C++的一点就是,Java是可以自动进行垃圾回收的。所以在Java中的内存泄露概念和C/C++中的内存泄露概念不一样。在Java中,一个对象的指针一直被应用程序所持有得不到释放就属于是内存泄露。而C/C++则是把对象指针给弄丢了,该对象就永远无法得到释放,这就是C/C++里的内存泄露。
在进行垃圾回收的是时候,要如何确认一个对象是否是垃圾呢?在很久以前有一种方式就是使用引用计数,当一个对象指针被其他对象所引用时就会进行一个计数。在进行垃圾回收时,只要这个计数存在,那么就会判断该对象就是存活的。而没有引用计数的对象,就会被判断为垃圾,可以进行回收。但是这种方法缺陷很明显,计数会占用资源不说,如果当一个A对象和一个B对象互相持有对方引用时,那么这两个对象的引用计数都不会为0,就永远不会被回收掉,这样就会导致内存泄露的问题。
在Java中,则是采用枚举根节点的方式:
如上图,JVM会从根节点开始遍历引用,只要顺着引用路线所遍历到的对象都会判断为存活对象,即是具有可达性的,这些对象就不会被回收。而没有被遍历到的对象,也就是图中的E和F对象,即便它们俩互相都还存在引用,也会被回收掉,因为它们不存在根节点的引用路线中,即是不具有可达性的。
既然了解了JVM如何判断一个对象是否为垃圾后,我们就可以来看看一些垃圾回收算法了:
1.标记-清除:
2.复制算法:
3.标记-整理:
4.分代垃圾回收:
在上一小节了解了一些常见的垃圾回收算法后,我们再来看看JVM中常见的垃圾收集器:
注:串行收集器几乎不会在web应用中使用,所以主要介绍并行和并发收集器
串行 VS 并行 VS 并发:
停顿时间 VS 吞吐量:
开启串行收集器:
开启并行收集器:
并发收集器在JDK1.8里有两个,一个是CMS,CMS因为具有响应时间优先的特点,所以是低延迟、低停顿的,CMS是老年代收集器。开启该收集器的参数如下:
另一个是G1,开启该收集器的参数如下:
注:实线代表可搭配使用的,虚线表示当内存分配失败的时候CMS会退化成SerialOld。JDK1.8中建议使用的是G1收集器
有这么多的垃圾收集器,那么我们要如何去选择合适的垃圾收集器呢?这个是没有具体答案的,都得按照实际的场景进行选择,但一般都会按照以下原则来进行选择:
其中并行收集器是支持自适应的,通过设置以下几个参数,并行收集器会以停顿时间优先去动态调整参数:
当内存不够的时候并行收集器可以动态调整内存,虽然实际生产环境中用的比较少,至于每次动态调整多少内存,则使用以下参数进行设置:
了解了并行收集器后,我们来简单看看CMS收集器其他的一些特性以及相关调优参数。
CMS垃圾收集过程:
CMS的缺点:
CMS的相关调优参数:
设置并发的GC线程数:
开启以下参数可以在Full GC之后对内存进行一个压缩,以此减少空间碎片:
这个参数则是设置多少次Full GC之后才进行压缩:
设置Old区存满多少对象的时候触发Full GC,默认值为92%:
启用该参数表示不可动态调整以上参数的值:
启用该参数表示在Full GC之前先做Young GC:
在jdk1.7之前可以使用以下参数,启用回收Perm区:
在jdk1.8后,推荐使用的垃圾收集器是G1。G1收集器在jdk1.7中第一次出现,所以到了jdk1.8里就非常成熟了。
G1收集器官网介绍如下:
The Garbage-First (G1) garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with high probability, while achieving high throughput. Whole-heap operations, such as global marking, are performed concurrently with the application threads. This prevents interruptions proportional to heap or live-data size.
The first focus of G1 is to provide a solution for users running applications that require large heaps with limited GC latency. This means heap sizes of around 6GB or larger, and stable and predictable pause time below 0.5 seconds.
官方文档地址:
http://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html
原理概述:
G1 也是属于分代收集器的,但是G1的分代是逻辑上的,而不是物理上的
G1 将整个对区域划分为若干个Region,每个Region的大小是2的倍数(1M,2M,4M,8M,16M,32M,通过设置堆的大小和Region数量计算得出。
Region区域划分与其他收集类似,不同的是单独将大对象分配到了单独的region中,会分配一组连续的Region区域(Humongous start 和 humonous Contoinue 组成),所以一共有四类Region(Eden,Survior,Humongous和Old),G1 作用于整个堆内存区域,设计的目的就是减少Full GC的产生。在Full GC过程中由于G1 是单线程进行,会产生较长时间的停顿。
G1的OldGc标记过程可以和yongGc并行执行,但是OldGc一定在YongGc之后执行,即MixedGc在yongGC之后执行。
结构图:
G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,尽可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:
G1的几个概念:
G1中的Young GC过程,和以往的是一样的:
但是G1中没有Full GC,取而代之的是Mixed GC:
G1里还有一个概念叫全局并发标记(global concurrent marking),和CMS的并发标记是类似的:
G1相关调优参数:
设置堆占有率达到这个参数值则触发global concurrent marking,默认值为45%:
设置在global concurrent marking结束之后,可以知道Region里有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数的值,只有达到了,下次才会发生Mixed GC:
设置Old区的Region被回收时的存活对象占比:
设置一次global concurrent marking之后,最多执行Mixed GC的次数:
设置一次Mixed GC中能被选入CSet的最多Old区的Region数量:
其他参数:
注意事项:
至于是否需要切换到G1收集器,可以根据以下原则进行选择:
关于在Web应用中,如何判断一个垃圾收集器的好坏,主要是看以下两点,以下两点都需为优才是好的垃圾收集器: