Java虚拟机的一个强大之处在于其提供垃圾自动回收,对开发人员掩盖了内存分配和回收的细节。本篇将探索JVM的内存分配和垃圾回收机制,以在内存分析实战中提供一些理论和实践参考。
1. 从理论开始
1.1.垃圾检测
任何虚拟机的回收算法都包括两个步骤:检测垃圾和回收垃圾。当一个对象被创建时,其是活动的对象,此时其是可用的,而在运行过程中,该对象的引用不再被使用,这时该对象就成为垃圾,一般采用两种方式来区别活动对象和垃圾对象:引用计数和跟踪。当一个对象被其它对象引用时,其引用计数加1,当不再被其它对象引用时,计数减1,当其引用计数为0时,该对象就成为垃圾,引用计数的缺点是无法检测循环引用和维护引用计数的代价,在现代虚拟机中一般不采用该方式。而跟踪指的是虚拟机追踪从根对象出发的有向对象图,如果一个对象不能被追踪到,则该对象为垃圾,采用追踪算法的垃圾回收器也叫标记并回收回收器(见下图,可以形象地理解为在堆空间中引入了重力场,参见http://developer.51cto.com/art/200610/32793.htm)。
图1 初始状态
图2 TR1-A链和TR2-D链断开,A、B、C、D掉入回收池
图3 A、B、C、D四个对象被回收
1.2.避免堆碎片
在进行垃圾回收之后,由于内存分配时,垃圾对象和活动对象可能相邻存在,则可能会在堆中出现堆碎片,采用标记并回收回收器的虚拟机一般采用两种方法来对付堆碎片:压缩和拷贝。压缩回收是指当进行垃圾回收之后,将剩下的活动对象推向堆的另一端,则当垃圾回收完毕之后在另一端形成连续的堆。拷贝回收指的是保持两个同样大小的堆,在对一个队进行垃圾回收过程中,对于检测出来的活动对象拷贝到另一个堆并连续存放。参见下图
图4 拷贝收集和压缩手集
1.3.分代回收
拷贝回收的一个缺点在于有些对象是长期存在的,但在回收过程中仍然必须将这些对象多次拷贝,分代回收指的是,对不同年龄(在内存存在的时间的长短)将对象放入不同的代堆中,并对不同的代堆采用不同的回收算法。事实上,大部分的对象属于“短命”对象,我们可以更多地对年轻的代堆进行垃圾回收,而对于年老的代堆则减少回收频度。
1.4.垃圾回收器设计决策
顺序执行垃圾回收VS并行垃圾回收:在顺序执行垃圾回收器中,即使系统拥有多个处理器,也只有一个处理器会执行垃圾回收,相比之下,并行垃圾回收具备更好的垃圾回收性能,但因此必定引入更多的复杂性和潜在的内存碎片。
并发执行VS停止并回收:停止并回收指在进行垃圾回收时停止应用线程的执行,而并发执行垃圾回收器在大部分情况下与应用线程并发执行,当然,部分操作仍然需要在停止应用线程的情况下执行,并发回收对应用线程的停止时间更短,但相比而言,并发执行垃圾回收器将占用更多的空间并有可能对系统性能产生影响。
压缩VS不压缩VS拷贝:随着对象的消亡,JVM的堆空间将不可避免地存在碎片:一种方式是对碎片进行压缩,付出的代价是垃圾回收时更多的操作,得到的好处是能够快速分配;一种方式是不进行压缩,好处是垃圾回收更快,但分配算法将更加复杂;另外一种是拷贝,即将存活的对象拷贝到一个新的空间段上,之后将旧的对象全部销毁,好处是垃圾回收快,内存分配快,但必须付出更多的空间代价。
1.5.性能度量
吞吐量(Throughput):吞吐量 = (总执行时间-垃圾回收时间)/总执行时间
暂停时间(Pause time):垃圾回收导致的应用的暂停时间
2. Sun HotSpot JVM实战
2.1.JVM分代垃圾回收
JVM的堆空间分成年轻代(Young Gen)、年老代(Old Gen)和持久代(Perm Gen),其中年轻代用于放置新创建的对象,而对象在经过几次的垃圾回收后仍然没有被回收,则将其转移到年老代,持久代用于放置一些为JVM本身的方便性而使用的对象,譬如类定义、方法定义、常量、代码段等。年轻代又分为一个Eden区和两个Survivor区, 而总有一个Survivor区是空的,随着每一次的垃圾回收,从Eden区和非空Survivor区的存活对象拷贝到空Survior区,如果Survior空间不足,则直接拷贝到年老区中,并将刚经历过回收的区空间返回到堆空间,此时两个Survivor的角色做了个转换(空的变成非空,非空的变成空的)。当一个对象经过多次回收仍然存在,则将其转入年老区。Sun HotSpot JVM对不同代将使用不同的垃圾回收算法。
图5 Sun HotSpot VM堆分代
图6 年轻代垃圾回收示意图
2.2.内存分配
对于大块的连续内存,使用bump-the-pointer技术,内存分配将非常简易和快速。对于多线程的场景,情况稍微复杂,内存分配必须是线程安全的,这可能导致内存分成为系统的瓶颈。Sun HotSpot JVM使用所谓的Thread-Local Allocation Buffers (TLABs)技术来解决这个问题,通过指定给每个线程一定的Buffer空间,线程在内存分配时,将尽量从此空间上分配,由于空间是独享的,因此分配将非常地快速,当然相应的,可能会造成空间的浪费。
2.3.不同垃圾回收器比较
2.3.1.垃圾回收方式
垃圾回收器名称 年轻代回收 年老代回收 压缩
Serial Collector 单线索程,顺序回收 单线程,顺序回收 是
Parallel Collector 多线程,并行回收 单线程,顺序回收 是
ParallelCompacting Collector 多线程,并行回收 多线程,并行回收 是
Concurrent Mark-Sweep (CMS) Collector 多线程,与应用并发回收 多线程,与应用并发回收 否
2.3.2应用场景
Serial Collector:适用于客户端应用程序,并且对暂停时间要求不严格,能够有效地管理64M左右的内存,垃圾回收导致的暂停时间在半秒内
Parallel Collector:适用于多处理器的服务器,并且对暂停时间要求不严格,譬如批处理等
ParallelCompacting Collector:适用于多处理器的服务器,并且对暂停时间稍微严格的应用
Concurrent Mark-Sweep (CMS) Collector:适用于多处理器的服务器,并且对暂停时间非常严格的应用
2.3.3.Sun HotSpot JVM参数自动调整
2.3.3.1.server和client模式
除了32位的windows系统,只要服务器拥有2+个处理器单元和2G+的系统内存,虚拟机自动识别为server模式,否则为client模式
可以通过在启动参数中指定-server或者-client来指定使用的模式
Client模式默认使用serial garbage collector,初始4M、最多64M的堆内存
Server模式默认使用parallel collector,初始1/64的物理内存最多不超过1G、最多1/4的物理内存最多不超过1G
可以通过指定特定的参数来覆盖默认的参数
2.3.3.2.Parallel Collector根据设定的目标自动调整
指定暂停时间目标:通过指定-XX:MaxGCPauseMillis=n启动参数来设定最大的暂停时间目标,单位为毫秒,虚拟机自动根据该目标来调整各个代空间的大小,当然,有时会也会无法达到目标,另外,该参数可能会降低系统的吞吐量。默认情况下,虚拟机不指定暂停时间。
指定吞吐量目标:通过指定-XX:GCTimeRatio=n 启动参数来设置最低的吞吐量目标,其中,n = 1 / (1-吞吐量) – 1,譬如预定的吞吐量为95%,则n = 1 / 5% - 1 = 19,虚拟机自动根据该目标自动调整各个代空间的大小,注意,该参数可能会加到垃圾回收导致的暂停时间。虚拟机默认目标吞吐量是99%。
2.4.常用的Sun HotSpot JVM启动参数
2.4.1.指定垃圾回收器
–XX:+UseSerialGC:使用Serial Collector
–XX:+UseParallelGC:使用Parallel Collector
–XX:+UseParallelOldGC:使用ParallelCompacting Collector(JDK6+)
–XX:+UseConcMarkSweepGC:使用Concurrent Mark-Sweep (CMS) Collector
2.4.2.Parallel and Parallel Compacting Collectors参数
–XX:ParallelGCThreads=n(默认为处理器数):并发垃圾回收线程数
–XX:MaxGCPauseMillis=n和–XX:GCTimeRatio=n:见2.4.2说明
2.4.3.CMS Collector参数
–XX:+CMSIncrementalMode(默认为disable):启用垃圾回收增量模式,该模式可降低因垃圾回收导致的暂停时间
–XX:ParallelGCThreads=n(默认为处理器数):并发垃圾回收线程数
–XX:+CMSIncrementalPacing(默认为disable):与上一个参数配合使用,根据应用程序的行为自动调整每次执行的垃圾回收任务的幅度(或比例?)(原文:Enables automatic control of the amount of work the CMS collector is allowed to do before giving up the processor, based on application behavior.)
-XX:CMSIncrementalDutyCycle=<N> (default: 5):每次增量回收垃圾的占总垃圾回收任务的比例,如果参数CMSIncrementalPacing指定,则指的是初始比例。(原文:This is the percentage (0-100) of time between minor collections that the concurrent collector is allowed to run. If CMSIncrementalPacing is enabled, then this is just the initial value.)
-XX:CMSIncrementalDutyCycleMin=<N> (default: 10):与CMSIncrementalPacing参数配合使用,每次增量回收垃圾的占总垃圾回收任务的最小比例(原文:This is the percentage (0-100) which is the lower bound on the duty cycle when CMSIncrementalPacing is enabled.)
2.4.4.垃圾回收统计(系统剖析和性能调优使用)
–XX:+PrintGC:打印每次垃圾回收的基本信息
–XX:+PrintGCDetails:打印每次垃圾回收的详细信息
–XX:+PrintGCTimeStamps:在每次垃圾回收时打印时间,与上两个参数之一结合使用
2.4.5.堆空间和代空间设置
–Xmsn:初始堆空间,譬如-Xms512M
–Xmxn:最大堆空间,譬如-Xmx1024M,一般可指定这两个参数一致以避免在系统运行期间进行堆空间的调整
–XX:MinHeapFreeRatio=minimum(默认40):当代空闲空间在代空间中比例大于maximum时,自动减少代空间以小于该值
–XX:MaxHeapFreeRatio=maximum(默认70):当代空闲在代空间中比例小于minimum时,自动增长代空间以大于该值
–XX:NewSize=n:默认年轻代空间的大小,譬如–XX:NewSize=400M
–XX:NewRatio=n(默认server模式是8,client模式是2):年轻代空间与年老代空间的比例
–XX:SurvivorRatio=n(默认是32):Eden区与Survivor区的比例,譬如–XX:SurvivorRatio=7,则每个Survivor区占整个年轻代空间的1/9(注意,有两个Survivor区)
–XX:MaxPermSize=n:持久区大小,譬如–XX:MaxPermSize=128M
3. 装备我们的工具箱
3.1.内存剖析(jmap+jhat)
3.1.1.命令
Jps:与ps命令类似,显示java进程的进程号
Jmap: 统计当前虚拟机的堆内存数据,一般使用如下:jmap –dump:file=filename {pid},将统计数据dump到filename文件中
Jhat(Java Heap Analysis Tool):将上一个命令的统计数据文件分析后以html的方式展现,一般使用如下:jhat –J-mx512M filename
3.1.2.常用的统计数据
在jhat程序运行后,访问http://{server}:7000查看统计数据
3.1.2.1.每个类的实例数(按实例数从多到少排序)
图7 类实例数统计截图
3.1.2.2.每个类的实例数和占用空间数(按占用空间数从大到小排序)
图8 类实例占用空间统计截图
3.1.2.3.每个类的引用统计(如下是String被其它类的实例引用的次数统计,按引用次数从多到少排序)
图9 类java.lang.String引用截图案
3.2.虚拟机监控(jconsole)
http://java.sun.com/j2se/1.5.0/docs/guide/management/jconsole.html
3.3.综合剖析工具(jprofiler)
Jprofiler是一个功能非常强大的性能剖析工具,但其对系统的性能影响非常之大,一般只能用于测试环境的性能剖析,特别是执行时间统计剖析。入门使用可参见如下的链接
http://www.blogjava.net/ddpie/archive/2007/05/14/117450.html
4. 参考文档
<<Memory Management in the Java HotSpot™ Virtual Machine>>
<<Java SE 6 HotSpot™ Virtual Machine Garbage Collection Tuning >>
<< Garbage Collector Ergonomics>>
<<深入JAVA虚拟机>>
JVM内存管理和垃圾回收
最新推荐文章于 2022-02-25 01:13:03 发布