垃圾回收:JVM的垃圾回收主要发生在堆内存中,绝大多数情况下都是对堆内存中的对象进行回收,jvm会监控我们堆中的对象是否存在引用,是否可达,检测到不可达的对象则将其丢进一个回收队列当中进行垃圾回收,但是垃圾回收会阻塞我们的程序,因此频繁的进行垃圾回收会严重影响我们系统的性能,JVM给我们提供了很多的优化方法;
JVM对垃圾的“定义”分析:
1、引用计数的判定策略:这是早起的一个算法,在我们的堆中存活的对象都会有一个引用计数,当每增加一个引用指向该对象则引用数量+1,当一个引用回收是则引用数量-1,当这个引用计数为0时,则认为这个实例可以被垃圾回收。但无法解决循环引用的问题。
2、可达性分析算法:我们将我们线程栈中的引用(引用变量)、方法区中类静态属性引用的对象、方法区中常量引用的对象等作为一个GC Root开始去堆中寻找它的引用对象,找到之后作为一个节点,然后再这个节点里面继续找引用对象,循环下去,只到最后不存在引用了这样就形成了类似于一棵树的对象树的结构。当所有的GC Root下的节点寻找完毕之后,堆中不属于任何对象树上的节点则认定为对象不可达,这些不可达的对象则是垃圾回收的对象。这是可达性分析算法
常用的垃圾回收算法:标记-清除算法、复制算法、标记-整理算法、分代垃圾回收算法
1、标记-清除算法:标记清楚算法使用可达性分析算法从GC Root开始扫描,标记那些可达性对象,标记完之后,对整个空间中为被标记的对象进行回收,回收掉不可达的对象,回收完之后不会进行对象的移动,在大多数情况下存活的对象占大多数,因此这种算法容易产生大量的内存碎片
2、复制算法:复制算法也是使用可达性分析算法标记存活的对象,他为了解决内存碎片的问题直接把整个内存空间分为两块,将存活的对象复制到另一块内存中,然后将这块内存区域全部清除。因此这样会浪费掉百分之五十的内存。
3、标记整理算法:标记整理法也是使用可达性分析算法对存活的对象进行标记,它同时解决了内存碎片和复制导致一半内存浪费的情况。标记整理在清除掉那些非存活的对象之后,会将所有存活的对象往左端空闲地址移动,并更新对应的指针地址,涉及到内存对象的移动,成本很高导致性能一般。
4、分代垃圾回收策略:我们创建出的对象生命周期的长短不一样,为了提高我们的垃圾回收效率,避免和减少频繁的进行垃圾回收;我们针对不同的生命周期的对象采取不同的回收策略;因为我们将对象分为年轻代、老年代、持久代这三代。并且在jvm堆中做对应的分区存储这些不同代的对象。
我们将年轻代分为三个区:一个Eden区、二个Survivor区(也可以设定多个)。大部分对象创建的时候在Eden区、当Eden区满了以后或者第一次GC时,还存活的对象会被复制到一个Survivor区,当这个Survivor满了之后,这个Survivor区存活的对象会复制到另一个Survivor区,以此循环,每循环一次我们认为成长了“一岁”、我们可以设定经过了多少次循环,多少代之后还存活的对象复制到老年代所在的年老区(Tenured),将年老区中的对象认定为生命周期比较长,所以我们GC的频率可以降低,和选择性能更加的垃圾回收算法;持久代(Permanent )内存中一般存放我们的静态资源,这一块对垃圾回影响很小,久代大小通过-XX:MaxPermSize=进行设置,这是分代分区域的对堆中的对象进行垃圾回收策略;
触发垃圾回收的时机:我们对对象回收进行了分代处理,对于不同区域的回收时机和回收算法的选择也是不一样的。我们一般在年轻代中选择Scavenge GC;当我们创建对象时去Eden区申请内存空间失败时。就会触发Scavenge GC。对Eden内存区进行一次垃圾回收,将存活的对象复制到Survivor区,如上所述,但不会对年老代或者持久代进行垃圾回收、因为新创建的对象都会存放在Eden区开始一个生命周期,所以我们一般不会分配很大的内存给这个区域,所以会进行一个频繁的GC过程把生命周期短的对象进行回收和一个筛选分代的过程。因为GC评率高,因此我们会选择使用速度快,效率高的垃圾回收算法。Full GC:Full GC会对整个堆内存进行遍历垃圾回收、进行内存整理;所以回收的效率会很慢,特别是内存数据比较大的时候,因此我们尽量减少full gc的调用次数。一般来说,full gc调用的原因会有:年老代Tenured内存区或者持久代Perm区域被写满了、触发full gc进行垃圾回收,我们在程序中显示的调用System.gc()也会触发full gc、上一次GC之后,整个堆的各个区域的内存分配策略发生变化。
垃圾回收处理器:串行处理器、并行处理器、并发处理器
1、串行处理器:单线程处理所有的垃圾回收,无需多线程交互,效率高、安全,一般用在小数据了的情况下,因为在垃圾回收的时候会阻塞用户线程,可以使用-XX:+UseSerialGC 打开;适用于:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用;缺点:只能用于小型应用
2、并行处理器:多线程同时进行垃圾回收,使用XX:+UseParallelGC/-XX:+UseParallelOldGC 打开,使用-XX:ParallelGCThreads=n 设置并行处理的线程数。适用于:对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算,缺点:垃圾收集过程中应用响应时间可能加长
3、并发处理器:这个是为了在保证应用程序不阻塞的情况下使用独立的垃圾回收线程进行垃圾回收,应对那些对响应时间要求、规模比较大的应用,使用-XX:+UseConcMarkSweepGC打开。适用于:对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用
常见的配置:
堆设置
-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:设置持久代大小
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
调优总结:
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
- 并发垃圾收集信息
- 持久代并发收集次数
- 传统GC信息
- 花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩