当前位置:文档之家› Java5 Garbage Collection(垃圾回收)

Java5 Garbage Collection(垃圾回收)

Java5 Garbage Collection

Java5 Garbage Collection (1)

Java 运行状态监测工具 (3)

jps (3)

jstatd (3)

jstat (3)

jconsole (4)

Linux下特有的JDK工具 (4)

jinfo (4)

jmap (4)

jstack (4)

jsadebug (5)

Java Garbage Collection Introduction (5)

简介 (5)

衡量GC的要素 (5)

暂停时间 (5)

暂停的可预见性 (6)

CPU耗用 (6)

堆内存的使用率 (6)

虚拟内存交互 (6)

Cache内存交互 (6)

内存对象整理 (6)

编译器介入和运期期辅助 (6)

常用的Garbage Collection的算法 (6)

标记非活动内存块 (6)

清理非活动对象 (7)

Garbage Collection in JDK5 (8)

基于Generation的 GC收集 (8)

Minor GC (8)

Major GC( Full GC) (9)

Young Generation Guarantee (9)

JDK的垃圾回收器( Collector) (10)

Serial Collector (10)

Throughput Collector (10)

Concurrent Collector (10)

Tunning In Sun JDK5 (11)

简述 (11)

应用程序调整案例 (12)

案例描述 (12)

分析 (12)

调整应用程序参数实践 (13)

step 1:侦测应用程序缓存数据的大小 (13)

step 2:完整地启动应用程序,并使用GC打印选项 (13)

step 3:调整Young Generation以及Tenured Generation的大小 (15)

step 4:使用Concurrent GC (16)

step 5:调整Young Generation的大小 (18)

step 6:调整Young Generation的大小 (19)

step 7:调整其它的参数 (23)

悬而未决的问题 (24)

GC的日志挖掘工具 (24)

参考的书籍 (25)

Java 运行状态监测工具

工欲善其事,必先利其器。Java应用程序的重要特点就是由于JVM的存在,调试手段非常多样化。善用这些工具,可以极大提高开发、除错的效率。

jps

列出所有的hot-spot JVM实例

实例:

jps (不带参数)

列出本机上所有的hot-spot Java实例

jps testserver1

列出远程服务器testserver1上的所有hot-spot Java实例,用RMI协议连接默认1099端口

jstatd

启动一个hot-spot JVM Monitor,它是一个基于RMI的应用程序,作用是向远程提供本地运行的hot-spot Java应用程序的信息

实例:

jstatd -J-Djava.security.policy=jstatd.policy

其中jstatd.policy需要自己新建,内容如下:

grant codebase "file:${java.home}/../lib/tools.jar" {

permission java.security.AllPermission;

};

这是一个安全策略文件,因为JDK本身对JVM的一些行为作了JAAS的安全检测,所以我们必须设置一些策略,使得jstatd被允许做一些网络的操作

另外必须注意的是,启动jstatd的机器上,必须能够反解它自己的主机名,且该主机名不是localhost而是它的对外服务IP。比如,在linux系统下,如果该主机ip为192.168.0.1,那么hostname i 应该返回192.168.0.1

jstat

这是JDK里面最实用的一个工具类,它直接提供对本地Java应用的状态观察,也提供基于jstatd对远程应用状态做观察。它可以观察到很多东西,包括classloader、compiler相关、gc 相关的信息,这其实已经覆盖了Java很大部分的状态监测

使用例子:

jstat -class 18695 1000 100

对PID为18695的Java进程做classloader的状态侦测,以1000ms为周期,做100次

jstat -compiler 18695 1000 100

对PID为18965的Java进程做java compiler的状态侦测,以1000ms为周期 ,做100次

jstat -gcutil 18695 1000 100

对PID为18965的Java进程做GC的状态侦测,以1000ms为周期 ,做100次

jconsole

其实是上述工具图形化的结果,它可以同时支持对上面那信息的抓取,直接使用,在图形界面里面决定连接到哪个进程进行监测。

Linux下特有的JDK工具

jinfo

观察一个运行中的Java应用程序的运行环境参数,或者也可以对一个core dump文件做当时的环境参数诊断

样例:

jinfo 18695

jmap

观察一个运行中的JVM对物理内存的占用情况,或者也可以对一个core dump文件做当时内存分布情况诊断

样例:

jmap 18695

这样可以观察到JVM的本地库文件所占内存的实际情况

jmap -heap 18695

这样可以观察到JVM内部当前各个内存块的情况

jstack

观察一个运行中的JVM当前的线程情况,或者也可以对一个core dump文件做当时的线程情况诊断

样例:

jstack 18965

jsadebug

附着到一个本地的Java进程,成为一个远程调试的debug server agent,然后远程的客户端可以通过jinfo、jmap、jstack这样的工具来得到被调试的Java进程的相关信息

样例:

jasdebug 18695 mytest

然后远程的客户端比如jinfo就可以通过

jinfo mytest@jasdebug_server_ip

来查看被附着的Java进程的信息了

Java Garbage Collection Introduction

简介

众所周知,Java如同其它的动态内管理语言Lisp、Smalltalk、Eiffel等一样,使用了动态的内存垃圾回收(Garbage Collection,GC)机制:程序员可以在代码中任意的分配内存空间

而无须关心内存空间的回收。这样做的好处是显而易见的,将内存空间的管理从代码中解藕出来,同时又避免了程序员在内存回收上的过失,增加了可靠性,也提高了编码的效率。

可是内存管理的任务依然存在,它被交给了虚拟机(Java Virtual Machine,JVM)。JVM利用一定的策略来回收堆(heap)空间中那些已经不再活动的对象,所谓的不再活动就是指没有对象引用它们(unreachable object)。从通常意义上讲,程序员可以不关心这一方面的事情,但事实上,GC的策略会对应用程序运行期效率产生非常大的影响。

理想的GC当然应该是这样子的:不需要GC的暂停时间,不占用CPU时间,不占用JVM

的内存,不干扰Cache,不使堆空间比常规运行时刻膨胀。但,这样的模型是不可能存在的,只能说,我们可以在牺牲某些方面的性能,但在另一些方面做得更好些,而这些策略通常取决于我们对应用程序的期待。

衡量GC的要素

暂停时间

GC是否需要暂时停止JVM中的用户进(线)程,也就是所谓的stop-the-world,如果是,能不能给出一个界定的时间

暂停的可预见性

暂停是不是可以预见的,能不能选择用户进(线)程可接受的时间点

CPU耗用

GC要占据多少的CPU资源

堆内存的使用率

有些GC的算法会对heap划块,而且其中有些块将不能被用户进(线)程使用,这就会使heap的使用率下降,也就是说heap的使用率远远小于用户限定的堆内存的可使用值

虚拟内存交互

由于某些GC需要检查所有的内存对象,如果恰好有一些内存对象处于虚拟内存页(比如swap空间),那有可能将导致一次内存swap,这会产生效率问题

Cache内存交互

虽然也许我们的内存足够大,可以将整个应用程序都放在内存中,但是不要忘记GC本身的运作也需要一定的空间,有可能GC本身的运维使用户进(线)程产生了内存切换

内存对象整理

虽然GC的主要任务是清理内存非活动对象,但有些GC策略会让内存里的活动对象因此而受益,比如通过对内存对象位置的整理,减少了内存碎片,同时也就改善了内存的分配速度

编译器介入和运期期辅助

有些GC策略需要编译器的帮助,比如编译器在代码中植入一些额外的指令,而在运行期,GC通过这些额外指令的帮助来进行一些清扫工作,这也意味着甚至有可能会存在一些interface让程序员在代码中帮助提高GC的效率

常用的Garbage Collection的算法

所有的GC算法需要面对的问题都是类似的:1)如何标记出需要被清理的内存块 2)用什么样的方式清理这些内存块

标记非活动内存块

引用(Reference)计数

如果把为每一个内存块对象设立一个引用计数器,并且每当有对象引用这个内存对象的时候,就给计数器增加值,那么就可以很容易地统计出对象被引用的次数。但是这个方法在实际中很少用,因为它的缺点很明显:1)不能直接识别成环状的引用,如果A->B,B->A,那么

这两个对象的计数器永远不为零2)需要编译器的介入,为所有的引用增加额外的运行期指令,以便在适当的时候增、减引用计数

追踪(Tracing)对象

通过深度追踪那些root对象(包括程序寄存器内的对象,static对象,栈stack对象,jni的引用对象),来为所有heap中被引用到对象做标记,而heap中所有被标记的对象之外对象都被认为是非活动对象,这是现在大部分GC都比较常用的标记方式

清理非活动对象

Mark Sweep

Mark Sweep在标记阶段,先使用Tracing方式找出所有的非活动对象;然后在清理阶段,根据之前标记的结果,将所有非活动的对象加入free list。这个算法比较简单,但缺陷也十分

明显,在清理阶段GC仍然需要访问内存中所有的对象,因为它仍然需要根据内存对象是否活动再决定是否将它加入到free list。

对于Mark Sweep算法来说,随着GC的进行,内存中会有越来越多的碎片,到最后很有可

能即使有足够的总空闲内存,却没有足够的连续块内存响应“连续块内存申请”,而抛出“没

有足够的内存”这样一个异常

Copying

Copying算法将内存分为两个区域,我们可以称之from-space和to-space,所有的内存分配只在from-space。在标记阶段同样也使用Tracing方式找出from-space中所有的非活动对象;然后在清理阶段,根据标记的结果,将from-space中所有的活动对象拷贝到to-space。这也就

是说,决定拷贝数量多少(同样影响拷贝时间长短)的是活动对象,因为Copying算法并不理会非活动对象。from-space与to-space在一次拷贝结束之后,它们的身份就被互换。

对于Copying算法来说,除了避免对整个内存区域进行全对象扫描之外,也同时整理了堆空间,from-space拷贝到to-space的对象总是连续的,因而使后续内存分配的效率得到提高。

但是Copying算法的缺陷也显而易见,它使内存空间的利用率减小了一半

Mark Compaction

Mark Compaction类似于Copying算法,但它没有from-space和to-space。同样的,在标记阶段它使用了Tracing方式找出所有的非活动对象;然后在清理阶段,它将活动对象尽量向heap的底部移动,直到所有的活动对象都被挪动到heap的一侧。

Mark Compaction算法的结果是得到一个接近于Copying算法中to-space的内存区块,而且长时间处于活动状态的内存对象都会趋向于heap的底部,这使得后续的内存分配也可以接近于Copying算法的效率,但是Mark Compaction算法相对比较复杂

Garbage Collection in JDK5

在实际应用中,单一使用同一种GC策略往往过于简单,并不能应付大部分的实际情况,因此在JDK1.4和JDK5中,采用了多种GC策略混合的方式

基于Generation的 GC收集

长期的数据试验说明,很大一部分的对象都是在建立之后不久就变成了非活动对象,而来自IBM的这一统计数字甚至高达98%。因此,如果能够将这部分即生即灭的对象与那些长期生存的对象区分对待,那么就可以极大的提高GC的效率。

在JDK1.4及JDK5中,提出了将堆内存分为不同的Generation的GC策略:

对于那些即生即灭,生存周期比较短的对象,将它们归入Young Generation,而那些生存周期比较长的对象(也就是指经历了一定次数GC周期的对象),将它们归入Tenured Generation。同时根据它们的不同特性,采取不同的GC策略:

对于Young Generation,由于对象的生存周期短,因此在GC的回收期到来时,很大部分的对象都已经处于非活动状态,因此Young Generation的GC策略是基于infant mortality的,所以在Young Generation的内存块中,使用Copying算法,这样可以避免对所有的对象做扫描,如果对象的死亡率足够高,那么相应的,Copying算法的效率也越高。在Young Generation的GC被称为Minor GC。

对于那些经历了数次Minor GC而依然存活的对象,它们会被以某种形式被转移到Tenured Generation,在这个堆区域使用Marking-Compaction算法来完成GC。因为这里的对象生存周期相对都较长,一般认为极有可能在数个GC周期后,Tenured Generation的对象仍然生存,所以采取了标记并压缩的算法,这样虽然效率差一些,但是可以保证提供连续的大块空间。对Young Generation 和Tenured Generation各做一次GC被称为Major GC(Full GC)。

另外还有Permanet Generation,相对而言,这块区域比较稳定,很少涉及动态的回收,主要用来存放给JVM使用descript objects class和method。对于Spring、Hibernate这类需要动态类型支持的框架来说,需要在这个区域给它们保留足够用的空间。

Minor GC

JDK将Young Generation分为三块区域

Eden:所有新分配的对象初始都将在这个区域

Survivor1:也被称为from_space

Survivor2:也被称为to_space

在Minor Gc中,垃圾收集器(Collector)先标记所有的生存对象(包括Eden和

from_space),然后将Eden中的生存对象拷贝到to_space,而from_space中的对象也将被拷贝到to_space(除非from_space中的这些对象已经在Young Generation中生存太久,经历过数个Minor GC周期,那么它们将被拷贝到Tenured Generation空间中去),这样Eden和from_space都已经被清理了,而to_space中都是Young Generation中存活的对象,当然也许to_space并不足以容纳这么对象,那么总会有一些对象被放到Tenured Generation空间中去。此时,原来的to_space角色进行转变,它将在下次Minor GC中成为新的from_space,而原来的from_space摇身一变为to_space。因此总有一块Survivor Space是处于浪费的状态,另外,如果Survivor不能容纳所有的存活对象,那么放不下的对象将直接扔到Tenured Generation空间中去,所以如果Survivor空间过小,将会总有一些对象逃逸到Tenured Generation中去。

Minor GC发生的频率较高,除了因为Collector主动提交的Minor GC频率较高之外,也因为相对Tenured Generation它更容易装满,但是它的回收效率相比Tenured Generation要高一个数量级。

Major GC(Full GC)

Major GC发生的时候,将由Collector先对Young Generation做一次回收,然后才回收Tenured Generation区域的非存活对象。在Tenured Generation内存区域,并没有划分多个区域,仅仅只是通过Marking、Sweeping、Compaction来回收空间。但是由于区域较大,而且通常对象都仍然生存,而且Sweeping和Compaction需要占用一定的时间,通常FULL GC引起的暂停时间对应用程序的影响更大。所以Collector主动提交的频率相对于Minor GC要低得多。

Young Generation Guarantee

有时候,Minor GC会引发Major GC。如果在Minor GC中,Collector发现Tenured Generation中保留的空闲空间小于Eden + from_space的空间大小,那么将引发一次Major GC。这是个悲观算法,因为Collector必须确保在Eden和from_space两个空间中所有对象仍然存活的情况下,Tenured Generation有足够的空间来接收这些对象。所以,如果将Young Generation的空间设置得过大,超过Tenured Generation的一半,那么每次都会触发Major GC,造成性能问题。

JDK的垃圾回收器(Collector)

JDK5提供的Collector分为

Serial Collector

JDK以前版本就存在的Collector,单线程完成Marking、Copying、Sweeping、Compaction所有工作,是默认使用的Collector,在单CPU的机器上使用比较合适。

在Serial Collector工作的时候,应用程序都将被挂起,整个JVM只有Serial Collector在工作。Throughput Collector

JDK5推荐使用的Collector,在Young Generation使用parallel模式Marking、Copying对象,默认使用N(N=CPU数量)根线程;在Tenured Generation使用单线程

Marking、Sweeping、Compaction对象。

在Throughput Collector工作的时候,应用程序都将被挂起,整了JVM只有Throughput Collector在工作。

如果没有多颗CPU,由于并发锁的存在,它的效率反而不如Serial Collector。

注意,由于在Young Generation使用parallel模式的copying,所以有可能会使Tenured Generation产生fragment。

Concurrent Collector

JDK5中最适合服务器使用的Collector,也称I-cms,在Young Generation使用parallel模式Marking、Copying对象(类似于Throughput Collector);在Tenured Generation使用Concurrent模式Marking、Sweeping、Compaction对象。所谓的Concurrent,是指它除了两次很短时间的暂停,其它时间的垃圾回收工作将不挂起应用程序。

在Concurrent Collector清理Young Generation的时候,应用程序将被挂起,JVM进入pause

状态,只有Concurrent Collector在工作(类似于Throughput Collector,默认使用N根线程,N=CPU数量);然而,在清理Tenured Generation的时候,Concurrent Collector将垃圾回收

工作分成了一系列的动作步,每隔一个很小的间隙执行相应的动作:

1.init-marking,这时候需要暂时挂起所有的应用程序直到这个动作完成,这个时间极其

短暂

2.concurrent mark,这时候需要独占一颗cpu进行标记非活动对象,但应用程序不挂起

3.pre-clean,这时候做清扫非活动对象前的准备工作,但应用程序不挂起

4.remark,这时候需要暂时挂起所有的应用程序直到这个动作完成,这个时间相对init-

marking要长一些

5.sweep,这时候做清理动作,但是不挂起应用程序,可能会持续一段时间

6.reset,这时候复位所有的Concurrent Collector的状态,但是不挂起应用程序Tunning In Sun JDK5

简述

性能调整与应用程序结合非常紧密。应用程序的特征决定了性能调整的方向,因为要突出某些方面的特征,必须要牺牲另一些方面的性能,而所谓的性能调优,就是权衡并且平衡性能表现的过程。

通常我们会从吞吐量、边界响应时间来考量应用程序的性能。在一定的时间内,如果应用程序可以为更多的连接提供服务,那么它的吞吐量就大。边界响应时间则是每个用户从连接到获得响应当中所经历的最大延时。对于应用程序来说,平均响应时间越短,它的吞吐量则越大。而边界响应时间则直接决定了用户感观。

如果仅仅只是考虚最大程度地使用资源,那么我们的目标应当定位于吞吐量,因为最大的吞吐量意味着资源被最大幅度地使用。如果考虑用户感受,那么边界响应时间是最重要的,但是限定边界响应时间,往往意味着要牺牲吞吐量。

这是因为Java使用的是动态内存回收的机制,每隔一定的间隙之后,都会有一些停顿期,在这期间应用程序处于挂起状态。而这一停顿期并不是固定的,它受很多因素的影响,其中有很多甚至是相互茅盾的。比如,在Young Generation内存区域中,使用基于Copying算

法的回收,它并不需要遍历所有的对象,它的执行时间仅仅取决于活动对象的数量,所以非活动的对象越多,拷贝的效率更高;如果内存足够大,那么两次收集的间隙也越大,就会有更多的对象变成非活动对象,但是相应的,内存标记和扫描时间也会增加。这两者之间的增长与减少并非是确定的,最后起决定性影响的因素跟应用程序关系密切,只有通过实际的测试才会有结论。一般而言,将Young Generation的容量调大,将会使GC发生的频率放缓,而由于容量的增加,清理的速度会慢一些,可是在两次清理之间的间隙较长,会有更多的客户响应不受到GC的影响(平均响应时间较短,吞吐量比较大),但受到GC影响的客户响应会承受较长的延时;将Young Generation的容量调小,将会使GC发生的频率发生得更

频繁些,但是因为容量小,每次清理的速度加快,而使应用程序中断的时间变小,结果就是更多的客户响应受到GC的影响而增加一些响应时间(平均响应时间会上升一些,吞吐量变小了),但好处是由于中断时间变小,受到影响的客户响应的延时变小了。

JDK启动参数的会很大程度上影响GC的效率,也会直接影响到应用程序的性能,调整的过程应该完全取决于应用程序的的性能需求,设计合理的模型,然后在模型已经定位的情况下再比较参数值对性能的影响。

应用程序调整案例

案例描述

有一个应用程序,它缓存了大量数据,数量级约在600万左右,用户可以直接访问一个页面,这个页面随机返回一条缓存的数据。

整个web应用程序的实现如下:

?使用Webx作为这个web应用程序的框架

?缓存的数据为POJO对象,它仅仅包括了

ID(String),Name(String),Created(Date),Data(double)四个属性

?在screen中使用注入方法,得到缓存对象的container

?使用velocity做为前端页面渲染的工具

既然是web应用程序,我们当然希望用户体验较好,响应速度当然越快越好。

分析

为了用户体验,我们应当将响应速度界限在一个范围之内,然后在这个范围内希望获得最好的响应速度。

为了让应用程序能够正常启动,我们当然需要为应用程序分配足够的内存,因为缓存数据将占据大量的内存。

这个应用程序对内存的占用其实可以分为两大块,动态的和静态的。缓存的数据其实就是静

态的,它不会随着时间而改变;为了响应用户访问请求而生成的对象都是动态的,随着用户访问的结束变成垃圾对象。根据JDK5 GC的设计理念,那些响应用户访问的动态对象更适合用Young Generation来应付,而缓存对象,我们则希望它们尽理不要被GC处理,甚至不需要再被GC扫描,因为一旦应用程序在正常运行的状态之后,这些缓存对象几乎不再需要被回收。

调整应用程序参数实践

step 1:侦测应用程序缓存数据的大小

先在应用程序中初始化200万个缓存对象,然后观察对象占据的内存空间大小,然后再估算600万对象所占的内存空间大小。

修改run.conf,起动应用程序的参数如下:

JAVA_OPTS: https://www.doczj.com/doc/cf1589390.html,=run.sh -server -Dcom.sun.management.jmxremote.port=7009 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false 为了便于观察heap中的情况,打开了remote jmx,就是黄色阴影部分。

用jconsole连到jboss的jvm上,可以观察到内存占用情况。

新建200万个bean,然后记得点一下Perform GC,因为此时除了缓存的bean之外还有一些其它的对象,需要清理掉。

可以观察到,200万个bean对象大约占了407M左右,那么600百万对象大约占1221M,那么大致估算,如果使用1.5G的heap空间,至少应当为数据缓存对象保留1230M左右的空间。

step 2:完整地启动应用程序,并使用GC打印选项

修改run.conf,指定heap堆大小值,让heap固定在1.5G之上,启动时的参数如下:

JAVA_OPTS: https://www.doczj.com/doc/cf1589390.html,=run.sh -server -Xms1536m -Xmx1536m

-Dcom.sun.management.jmxremote.port=7009 -Dcom.sun.management.jmxremote.ssl=false

-Dcom.sun.management.jmxremote.authenticate=false -verbose:gc -XX:+PrintGCDetails -XX:-TraceClassUnloading

黄色阴影部分指出了heap堆固定的大小为1536M,也就是1.5G

蓝色阴影部分指出了需要打印GC信息

通过GC的日志信息可以看到,HotSpot自动为我们调整Young Generation为150M,而Tenured Generation大约为1515M,同样从GC信息里还可以看到,一次Full GC的时间大约为12秒

使用apache benchmark来测试一下:

ab -n 1000000 -c 10 -k http://testhost:8080/miniwebx/shuffle.htm

Concurrency Level: 10

Time taken for tests: 301.643468 seconds

Complete requests: 1000000

Failed requests: 484005

(Connect: 0, Length: 484005, Exceptions: 0)

Write errors: 0

Keep-Alive requests: 990006

Total transferred: 860862485 bytes

HTML transferred: 571912455 bytes

Requests per second: 3315.17 [#/sec] (mean)

Time per request: 3.016 [ms] (mean)

Time per request: 0.302 [ms] (mean, across all concurrent requests)

Transfer rate: 2787.02 [Kbytes/sec] received

Connection Times (ms)

min mean[+/-sd] median max

Connect: 0 0 0.0 0 17

Processing: 0 2 65.5 1 10212

Waiting: 0 2 65.5 1 10212

Total: 0 2 65.5 1 10212

Percentage of the requests served within a certain time (ms)

50% 1

66% 2

75% 2

80% 3

90% 4

95% 5

98% 8

99% 12

100% 10212 (longest request)

在压力测试的同时,观察一下JBoss的console输出,可以看到,Full GC时有发生,并且常常在10秒左右,理所当然的,压力测试最大的延时确实也在10秒左右。

注意,由于我们的缓存数量比较大,因此在应用程序启动之后,在开始的一些访问中,因为GC需要调整内存中数据的位置,包括缓存数据从Young Generation到Tenured Generation的转移,都需要一定的时间,这期间数据的波动较大,直到所有的缓存对象被沉淀到Tenured

Generation之后,应用程序才开始比较稳健,这时候响应时间也开始变得比较平稳

step 3:调整Young Generation以及Tenured Generation的大小

根据我们的计划,应当将缓存对象全部扔到Tenured Generation中去,这样我们可以分别对两个区域使用不同的GC策略:在Young Generation基于死亡率的快速的垃圾收集,而在Tenured Generation应当尽量避免垃圾收集,即使垃圾收集不可避免的时候,我们也要尽可能的使用并发收集策略,以控制暂停时间。

按照之前的计算,我们准备留给Tenured Generation区域1220M以上的空间,那么我们可以这么划分:给Young Generation分配256M内存,剩下的就都留给Tenured Generation,也就是有1280M。

修改run.conf,指定Young Generation的大小,启动时的参数如下:

JAVA_OPTS: https://www.doczj.com/doc/cf1589390.html,=run.sh -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:SurvivorRatio=6 -Dcom.sun.management.jmxremote.port=7009

-Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -verbose:gc -XX:+PrintGCDetails -XX:-TraceClassUnloading

其中,-XX:NewSize和-XX:MaxNewSize可以固定Young Generation的大小,而-

XX:SurvivorRatio=6相当的重要,因为默认情况下Survivor空间太小了,在我的实例中居然只有192KB,这样的大小非常不好,会造成Survivor的快速溢满,然后Collector会尝试将活动对象拷贝到Tenured Generation。这种情况下,根据Young Generation Guarantee,当Eden 满了并做Minor GC的时候,Collector会保证Eden + Survivor1的值不大于Tenured空闲空间,然而我们的Tenured Generation负荷是相当的高的,几乎没有办法保证256M左右的空闲,这将造成不停的Full GC来尝试释放Tenured空间,而且永远不成功,JVM基本陷于无响应状态。通过去掉-XX:Survivor=6可以证实这一点。

这样,通过jconsole,我们可以看到Tenured Generation正是1280M。

还是使用ab来做测试:

ab -n 1000000 -c 10 -k http://testhost:8080/miniwebx/shuffle.htm

Concurrency Level: 10

Time taken for tests: 324.874677 seconds

Complete requests: 1000000

Failed requests: 763688

(Connect: 0, Length: 763688, Exceptions: 0)

Write errors: 0

Keep-Alive requests: 990006

Total transferred: 860859880 bytes

HTML transferred: 571909850 bytes

Requests per second: 3078.11 [#/sec] (mean)

Time per request: 3.249 [ms] (mean)

Time per request: 0.325 [ms] (mean, across all concurrent requests)

Transfer rate: 2587.71 [Kbytes/sec] received

Connection Times (ms)

min mean[+/-sd] median max

Connect: 0 0 0.0 0 16

Processing: 0 2 76.2 2 10869

Waiting: 0 2 76.2 2 10869

Total: 0 2 76.2 2 10869

Percentage of the requests served within a certain time (ms)

50% 2

66% 2

75% 2

80% 3

90% 4

95% 5

98% 8

99% 11

100% 10869 (longest request)

结果仍然可以发现不时地会出现Full GC,并且暂停时间相当的长,还是在10秒左右,最长的响应时间还是在10秒左右。

step 4:使用Concurrent GC

在多CPU的机器上,Concurrent GC当然是相对比较好的选择,它在Young Generation使用并行垃圾收集,在Tenured Generation使用并发的垃圾收集,这样就大幅减少了暂停时间,使响应速度大幅提高。

修改run.conf,使用Concurrent GC,启动时参数如下:

JAVA_OPTS: https://www.doczj.com/doc/cf1589390.html,=run.sh -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:SurvivorRatio=6 -Dcom.sun.management.jmxremote.port=7009

-Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -verbose:gc -XX:+PrintGCDetails -XX:-TraceClassUnloading -XX:+UseConcMarkSweepGC -XX: +CMSIncrementalMode -XX:+CMSIncrementalPacing

+UseConcMarkSweepGC表示打开Concurrent GC的选项

+CMSIncrementalMode表示使用增量模式,即将ConcurrentMarkSweep的过程再切分成一小段一小段的,占用更少的暂停时间,但缺点是可能造成空间碎片以及更长的Tenured Generation GC时间

+CMSIncrementalPacing表示自动调整增量的幅度,这个会让GC自适应调整每一个增量步的频度,这是一个非常厉害的选项,随着服务器运行时间的增加,GC自动对历史数据统计,并自动计算步频

仍然使用10个并发用户的测试:

ab -n 1000000 -c 10 -k http://testhost:8080/miniwebx/shuffle.htm

Concurrency Level: 10

Time taken for tests: 338.131464 seconds

Complete requests: 1000000

Failed requests: 483500

(Connect: 0, Length: 483500, Exceptions: 0)

Write errors: 0

Keep-Alive requests: 990004

Total transferred: 860853873 bytes

HTML transferred: 571903853 bytes

Requests per second: 2957.43 [#/sec] (mean)

Time per request: 3.381 [ms] (mean)

Time per request: 0.338 [ms] (mean, across all concurrent requests)

Transfer rate: 2486.24 [Kbytes/sec] received

Connection Times (ms)

min mean[+/-sd] median max

Connect: 0 0 0.0 0 4

Processing: 0 2 5.0 2 274

Waiting: 0 2 5.0 2 274

Total: 0 2 5.0 2 274

Percentage of the requests served within a certain time (ms)

50% 2

66% 2

75% 3

80% 4

90% 6

95% 8

98% 14

99% 22

100% 274 (longest request)

从1百万次的访问测试中来看,虽然99%以上的响应时间都非常不错,但是最长的响应时间也达到了300ms左右。CMSIncrementalPacing起了不小的作用,它根据应用程序的繁忙程度自动计算了增量执行的步频,在压力非常大的情况下,它甚至直接调整了duty_cycle为100,也就是完全延迟到下次Full GC的时候。

观察GC调试信息,有时可能会频繁出现concurrent mode failure,不但持续时间相当长,而且连续出现使用应用程序几乎没有响应,这表示Concurrent GC强制使用了parallel收集的方法来回收Tenured Generation,这直接导致了每次>10秒的暂停时间。原因是Tenured Generation被占用的空间太多了,导致它的剩余空间与Eden + Survivor相比而引发Young Generation Guarantee(当Young Generation占满时,最多可达224M,但Tenured Generation 最多只会剩下110M左右),导致不停引发但总也不能解决问题的Full GC,因为Tenured Generation无法释放更多的空间。

虽然接下来继续使用10个并发用户的测试,会使GC聪明地暂时放弃持续Full GC的尝试,但如果真的有用户请求在Full GC的过程中发生,那么都将得到超长的响应时间。

step 5:调整Young Generation的大小

如果将Young Generation缩小到180M,那么根据SurvivorRatio=6来计算,Eden将有

135M,Survivor将有两块22.5M的,那么Young Generation Guarantee会要求Tenured Generation有158M的储备,而按前面的结果来看Tenured Generation可以准备空闲的空间在170M左右,基本能够符合要求。

修改run.conf,启动时参数如下:

JAVA_OPTS: https://www.doczj.com/doc/cf1589390.html,=run.sh -server -Xms1536m -Xmx1536m -XX:NewSize=180m -XX:MaxNewSize=180m -XX:SurvivorRatio=6 -Dcom.sun.management.jmxremote.port=7009

-Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -verbose:gc -XX:+PrintGCDetails -XX:-TraceClassUnloading -XX:+UseConcMarkSweepGC -XX: +CMSIncrementalMode -XX:+CMSIncrementalPacing

再次使用10并发用户来做测试:

ab -n 1000000 -c 10 -k http://testhost:8080/miniwebx/shuffle.htm

Concurrency Level: 10

Time taken for tests: 315.778613 seconds

Complete requests: 1000000

Failed requests: 764993

(Connect: 0, Length: 764993, Exceptions: 0)

Write errors: 0

Keep-Alive requests: 990005

Total transferred: 860852328 bytes

HTML transferred: 571902303 bytes

Requests per second: 3166.78 [#/sec] (mean)

Time per request: 3.158 [ms] (mean)

Time per request: 0.316 [ms] (mean, across all concurrent requests)

Transfer rate: 2662.23 [Kbytes/sec] received

Connection Times (ms)

min mean[+/-sd] median max

Connect: 0 0 0.0 0 1

Processing: 0 2 4.4 2 250

Waiting: 0 2 4.4 2 250

Total: 0 2 4.4 2 250

Percentage of the requests served within a certain time (ms)

50% 2

66% 2

75% 3

80% 3

90% 5

95% 7

98% 13

99% 21

100% 250 (longest request)

观察GC的调试信息,已经看不到有concurrent mode failure产生了,测试的结果也不错,百万次测试的最长延时仅在300ms左右。

但是还有美中不足,如果应用程序长时间闲挂,将会出现Full GC,而且由于concurrent mode failure而强制暂停。

step 6:调整Young Generation的大小

通过使用Concurrent GC,应用程序的暂停时间已经大幅减小。由于已经抑制了Full GC的产生,我们现在更关注的是Young Generation的GC性能。我们需要尝试看看,能不能通过减小Young Generation的大小,缩短每次Minor Gc的暂停时间,从而使响应速度的最大延时下

降一些。

修改run.conf,启动时参数如下:

JAVA_OPTS: https://www.doczj.com/doc/cf1589390.html,=run.sh -server -Xms1536m -Xmx1536m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:SurvivorRatio=6 -Dcom.sun.management.jmxremote.port=7009

-Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -verbose:gc -XX:+PrintGCDetails -XX:-TraceClassUnloading -XX:+UseConcMarkSweepGC -XX: +CMSIncrementalMode -XX:+CMSIncrementalPacing

使用10并发用户来做测试:

ab -n 1000000 -c 10 -k http://testhost:8080/miniwebx/shuffle.htm

结果如下:

Concurrency Level: 10

Time taken for tests: 284.711378 seconds

Complete requests: 1000000

Failed requests: 763408

(Connect: 0, Length: 763408, Exceptions: 0)

Write errors: 0

Keep-Alive requests: 990006

Total transferred: 860856737 bytes

HTML transferred: 571906707 bytes

Requests per second: 3512.33 [#/sec] (mean)

Time per request: 2.847 [ms] (mean)

Time per request: 0.285 [ms] (mean, across all concurrent requests)

Transfer rate: 2952.74 [Kbytes/sec] received

Connection Times (ms)

min mean[+/-sd] median max

Connect: 0 0 0.0 0 15

Processing: 0 2 3.6 2 178

Waiting: 0 2 3.6 1 178

Total: 0 2 3.6 2 178

Percentage of the requests served within a certain time (ms)

50% 2

66% 2

75% 2

80% 3

90% 4

相关主题
文本预览
相关文档 最新文档