JVM层的GC调优是生产环境上必不可少的一个环节,由于咱们须要肯定这个进程能够占用多少内存,以及设定一些参数的阀值。以此来优化项目的性能和提升可用性,并且这也是在面试中常常会被问到的问题。html
想要进行GC调优,咱们首先须要简单了解下JVM的内存结构,Java虚拟机的规范文档以下:java
https://docs.oracle.com/javase/specs/jvms/se8/html/index.htmllinux
在介绍JVM内存结构以前,咱们须要先知道运行时数据区这样的一个东西,它与JVM的内存结构有着必定的关联。不过它属因而一个规范,因此与JVM内存结构是有着物理上的区别的。运行时数据区以下:web
1.程序计数器(Program Count Register,简称PC Register):面试
2.虚拟机栈(JVM Stacks):算法
3.堆Heap:服务器
4.方法区(Method Area):多线程
5.运行时常量池(Run-Time Constant Pool):并发
6.本地方法栈(Native Method Stacks):oracle
了解了运行时方法区规范后,咱们接下来看看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应用中,如何判断一个垃圾收集器的好坏,主要是看如下两点,如下两点都需为优才是好的垃圾收集器:
下一篇: