JVM调优实战

理论篇

1.1 多功能养鱼塘-JVM内存

大鱼塘O可分配内存): JVM可以调度使用的总的内存数,这个数量受操作系统进程寻址范围、系统虚拟内存总数、系统物理内存总数、其他系统运行所占用的内存资源等因素的制约。

小池塘A(堆内存):JVM运行时数据区域,它为类实例和数组分配的内存。堆可以是固定大小的也可以是可变大小的。其中 Heap = {Old + NEW = { Eden , from, to } }

小池塘B(非堆内存):包括所有线程之间共享的一个方法区域和JVM为优化或内部处理所分配的内存。它存储每一个类的结构,如一个运行时的常量池、字段和方法数据、方法的代码和构造函数。这个方法区是逻辑上堆的一部分,但依赖于实现,一个JVM可以不去回收或者压缩它。像堆一样,方法区可以固定大小的,也可以是大小可变的。方法区不是必须是连续的,它们可以是不连续的。除方法区之外,JVM总是从非堆中分配用于优化和内部处理所需的内存。例如,JIT编译器为高性能的JVM代码转换存储成本地代码而分配的内存。 

查看大池塘O大小的方法为:

在命令行下用 java -XmxXXXXM -version 命令来进行测试,然后逐渐的增大XXXX的值,如果执行正常就表示指定的内存大小可用,否则会打印错误信息,示例如下: 

java -Xmx3072M -version。

当一个URL被访问时,内存申请过程如下: 

A. JVM会试图为相关Java对象在Eden中初始化一块内存区域

B. Eden空间足够时,内存申请结束。否则到下一步

C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收), 释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor

D. Survivor区被用来作为EdenOLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor

扫描二维码关注公众号,回复: 1018830 查看本文章

E. OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)

F. 完全垃圾收集后,若SurvivorOLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误

1.2 池塘中的鱼-程序中的对象

程序中运行的各种类实例称之为对象,每个对象都有不同的生命周期,有的存活时间长点,有的存活时间短点,这就想鱼塘中养的不同生长期的鱼一样,有的三个月就可以上市,有的鱼则需要6个月甚至更长的时间才能上市。JVM内存机制的设置就是为了要满足这种不同生命周期的对象对内存的需求,并使之能达到最大的性能表现。

1.3 养殖区域划分-JVM中的代

鱼塘主人为了充分利用现有的条件来赚取更多的利润,他需要喂养各种不同种类的鱼,于是又把鱼塘分割成了几块不同区域:“鱼苗养殖区”、“短中期养殖区”、“长期养殖区”,来养殖不同生长周期的鱼。JVM同样为了对各种不同生命周期的对象进行有效管理也划分了各种不同的区域,这就是“代”的概念,分别叫做:“青年代”、“老年代”、“持久代”,下面逐一介绍每个代的含义和作用。

短中期鱼苗养殖区-年青代(Young Generation)

年青代由一个Eden Space和两个Survivor Spaces组成,虚拟机初始时分配所有的对象到Eden Space,许多对象也是在这里死去。当它执行一个“minor GC”的时候,虚拟机将从Eden Space中移动一些残余的对象到其中的一个Survivor Spaces中。青年代就好像养鱼塘中的“中短期养殖区”一样,主人把鱼先投放到“短期养殖区”喂养,隔一段时间就开始下网捞出已经长成的那些鱼拿到集市去卖,这个过程就是从“Eden Space”中执行垃圾回收的过程。主人接着把捕捞之后剩下的“漏网之鱼”赶到“中期养殖区”继续喂养。这个“中期养殖区”就是“Survivor Spaces”,当然鱼在“中期养殖区”喂养一段时间后也要捞出那些长成的鱼去卖,这就是对“Survivor Spaces” 执行垃圾回收的过程。

Ps Eden Space: 这个内存池在对象初始化时被分配;

Ps Survivor Space: 这个内存池中包含着Eden Space 经过GC之后幸存下来的对象;

年轻代设置策略:对于响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达老年代的对象。对于吞吐量优先的应用尽可能的设置大,可达到Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

长期养殖区-老年代(老年代):、

虚拟机将在Survivor Spaces中生存足够长时间的对象移动到老年代的Tenured Spaces中。当Tenured Generation被填满,则将执行一个完全GC,这个完全GC非常的慢,因为它要处理所有存活着的对象,用的是串行标记收集的方式,并发收集可以减少对于应用的影响。

老年代设置策略:对于响应时间优先的应用老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。

最优化的方案,一般需要参考以下数据获得:

Ø 并发垃圾收集信息

Ø 持久代并发收集次数

Ø 传统GC信息

Ø 花在年轻代和老年代回收上的时间比例

Ø 减少年轻代和老年代花费的时间,一般会提高应用的效率

对于吞吐量优先的应用一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。

较小堆引起的碎片问题因为老年代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现碎片,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现碎片,可能需要进行如下配置: -XX:+UseCMSCompactAtFullCollection 使用并发收集器时,开启对老年代的压缩;-XX:CMSFullGCsBeforeCompaction=0上面配置开启的情况下,这里设置多少次Full GC后,对老年代进行压缩

/*监控实例*/

内存池名称: Tenured Gen

Java 虚拟机最初向操作系统请求的内存量: 3,538,944 字节

Java 虚拟机实际能从操作系统获得的内存量: 1,431,699,456 字节

Java 虚拟机可从操作系统获得的最大内存量: 1,431,699,456 字节。请注意,并不一定能获得该内存量。

Java 虚拟机此时使用的内存量: 1,408,650,472 字节

/*监控实例*/

实例说明:系统能获得的最大Tenured Generation空间大小为1.431G左右,此时使用已经1.408G,基本满了,所以在JVM执行串行标记垃圾收集时,系统响应速度会很慢!

鱼苗养殖区-持久代(Permanent Generation)

控制着所有虚拟机自己映射的数据,如类和对象的方法。持久代jvm则存储classmethod对象。持久代就像鱼苗养殖区一样,池塘主人一次对该区域投入足够量的鱼苗,已保证其他鱼塘的足够供应。就配置而言,永久域是一个独立域并且不认为是堆的一部分。永久域默认大小为4m.运行程序时,jvm会调整永久域的大小以满足需要。每次调整时,jvm会对堆进行一次完全的垃圾收集。 使用-XXMaxPerSize标志来增加永久域搭大小。在WebLogic Server应用程序加载较多类时,经常需要增加永久域的最大值。当jvm加载类时,永久域中的对象急剧增加,从而使jvm不断调整永久域大小。为了避免调整,可使用-XXPerSize标志设置初始值。

1.4 主人定期捕鱼-JVM垃圾回收

一个池塘收容积限制,能养殖的鱼的数量是一定的,因此隔一段时间必须捞出部分长成的鱼来使主人能喂养很多的鱼。同样,JVM所管理的有限内存也要实现最优化利用,Garbage Collection(GC)就是用来释放没有被引用的对象所占领的内存,目的在于清除不再使用的对象。GC通过算法和参数的配置可以对性能产生效果显著的影响。

GC就好像把长成的鱼从池塘中捞出来拿到市场上去卖,然后给池塘腾出空间继续养别的鱼赚钱。采用哪种养殖方式能让鱼塘主人赚到更大的利润是鱼塘主人的经营目的,而JVM调优的目的在于如何能是系统表现出更好的响应时间、更大的吞吐量。

Minor Collections(局部垃圾回收):当通用内存消耗完被分配的内存时,JVM会在内存池上执行一个局部的GC(总是调用minor collection)去释放被dead的对象所占用的内存。这个局部的GC通常比完全GC要快许多。青年代中的垃圾回收就是采用局部垃圾回收机制,因此,青年代中内存分配和管理效率也是最高。

通常情况下,对于内存的申请优先在青年代中申请,当内存不够时会整理新生代,当整理以后还是不能满足申请的内存,就会向老年代移动一些生命周期较长的对象。这种整理和移动会消耗资源,同时降低系统运行响应能力,因此如果青年代设置的过小,就会频繁的整理和移动,对性能造成影响。那是否把年青代设置的越大越好,其实不然,青年代采用的是复制搜集算法,这种算法必须停止所有应用程序线程,服务器线程切换时间就会成为应用响应的瓶颈。

Major Collections(完全垃圾回收):老年代需要被回收,这就是一个major collection ,它的运行常常非常慢,因为它要涉及所有存活着的类。

/*实例*/

垃圾收集器的名称: Copy(复制收集算法)

使用此垃圾收集器收集的数量: 219 字节

垃圾收集时间: 18 630 毫秒

垃圾收集器的名称: MarkSweepCompact(标记压缩算法)

使用此垃圾收集器收集的数量: 47 字节

垃圾收集时间: 36 166 毫秒

实例说明:copy垃圾搜集器的运行时间为18秒回收219字节,回收速度为平均每秒12字节,而MKC垃圾搜集器的时间为36秒回收了47字节,回收速度为平均每秒1.3字节,两者差距几乎达到了10倍,可见完全垃圾回收的速度远不如局部垃圾回收。 

1.5 不同的捕鱼方式-垃圾回收器

Sun JVM提供4垃圾回收器:

Serial Collector(序列垃圾回收器):垃圾回收器对Young Gen和Tenured Gen都是使用单线的垃圾回收方式,对Young Gen,会使用拷贝策略避免内存碎片,对Old Gen,会使用压缩策略避免内存碎片。在JVM启动参数中使用-XX:+UseSerialGC启用Serial Collector。串行收集器只适用于小数据量的情况,默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。基本上在多内核的服务器上应该避免使用这种方式。JDK5.0以后,JVM会根据当前系统配置进行判断。串行GC适合小型应用和单处理器系统(无需多线程交互,效率比较高)

Parallel Collector(并发垃圾回收器):垃圾回收器对Young Gen和Tenured Gen都是使用多线程并行垃圾回收的方式,对Young Gen,会使用拷贝策略避免内存碎片,对Old Gen,会使用压缩策略避免内存碎片。在JVM启动参数中使用-XX:+UseParallelGC启用Parallel Collector。这是一种吞吐量优先的并行收集器 ,主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。采用了多线程并行管理和回收垃圾对象,提高了回收效率服务器的吞吐量,适合于多处理器的服务器。

Parallel Compacting Collector(并行压缩垃圾回收器):Parallel Collector垃圾回收类似,但对Tenured Gen会使用一种更有效的垃圾回收策略,此垃圾回收器在暂停时间上会更短。在JVM启动参数中使用-XX:+UseParallelOldGC启用Parallel Compacting Collector。这是一种响应时间优先的并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

Concurrent Mark-Sweep (CMS) Collector(并发标志清除垃圾回收器):Young Gen会使用与Parallel Collector同样的垃圾回收策略,对Tenured Gen垃圾回收的垃圾标志线程与应用线程同时进行,而垃圾清除则需要暂停应用线程,但暂停时间会大大缩减,需要注意的是,由于垃圾回收过程更加复杂,会降低总体的吞吐量。

这里说一下并行和并发的区别,并指的是多个进程并行执行垃圾回收,那么可以很好的利用多处理器,而并指的是应用程序不需要暂停可以和垃圾回收线程并发工作

说明:对于联机处理的应用系统或复杂的3层应用系统,采用Concurrent Mark-Sweep (CMS) Collector进行垃圾搜集,基本上既能保证性能,又能保证稳定性(暂停时间短)。

猜你喜欢

转载自blog.csdn.net/u010682330/article/details/79494224