看了一下Oracle官方JDK 5.0的《Memory Management in the Java HotSpot™ Virtual Machine》,对HotSpot的Garbage Collection(GC)机制有一个初步的认识。为什么要去了解GC?主要还是想了解HotSpot是如何管理内存的,因为平时写代码,Java程序员只管new,至于对象放在哪里可能不太关注,这其实也影响代码质量,还是需要多了解一下JVM。
对C++程序员来说,经常碰到内存问题,容易出现两类问题:1)内存泄漏;2)使用已经释放内存空间的指针。C++代码中的内存管理是程序员的事情,你可以在堆上去new,但也有责任去释放。Java其中一个目标就是希望程序员将更多的精力放在自己的业务上,很多关键问题都由Java或JVM来解决,例如,JVM管理内存,其专门有一套GC机制去解决内存上的问题,C++中出现的很多内存问题可能JVM都已经帮助解决了,但是,严格地讲,放手让JVM去管理内存也还是欠缺稳妥的,Java程序员需要去了解GC机制,这样才能对自己写得代码更有把控力。
在JVM中,GC回收器负责三个方面的工作:1)分配内存;2)不回收被引用的对象;3)回收不再被引用的对象,其实就是分配内存,回收内存。
其性能衡量指标:
1)吞吐量(throughput):在一定的时间范围内,应用程序占用CPU时间(即未消耗在GC上的时间)的百分比;
2)GC开销(garbage collection overhead):GC占用CPU时间的百分比;
3)暂停时间(pause time):stop-the-world(应用程序暂停)的时间长;
4)回收频率(frequency of collection):相对于应用程序运行,GC发生的频率;
5)内存占用(footprint):堆(Heap)大小、方法区(Method area)大小;
6)及时性(promptness):一个对象从成为garbage到其所占用的内存又重新可用的时间间隔。
GC领域有以下两种现象:
1)大部分已分配的对象不会长时间被引用;
2)很少出现先分配的对象引用后分配对象。
这是业界熟知的“weak generational hypothesis”理论。另外年轻代(Young generation)、年老代(Old generation)各有自己的特点:年轻代因占用内存较小,存放较多已不再被引用的对象,相对于年老代,回收频率更高、回收速度更快、更高效;年老代因占用内存较大、增长较慢,所以回收频率较低,回收时间也长。那些经过一定次数年轻代回收后仍处于存活状态的对象会竞升到年老代(也就是对象一直在被引用,应用程序需要用):
对年轻代,倾向于选择体现回收速度的算法;而对年老代,则倾向于选择体现内存空间效率的算法,需要尽可能减少garbage占用及碎片。.
在HotSpot 内存中,分为三代:一个年轻代、一个年老代、一个永久代(a permanent generation)。大部分对象初次分配在年轻代;除了某些占用内存较大的对象会直接分配在年老代,另外,经过一定次数年轻代回收后仍处于存活状态的对象也会竞升到年老代;永久代主要是存放便于JVM查找信息的一些类数据(Class data)。年轻代、年老代都对应堆的内存,永久代是对应方法区的内存。年轻代还可以继续细分为一个eden区和两个survivor区,如下:
初次分配在年轻代的对象具体是分配到eden区,在两个survivor区中,任何时刻都只有一个区有对象,另一个区为空,为空的survivor区(To区)是便于回收中的对象复制,有对象的survivor区(From区)用来保存至少经过一次年轻代回收的对象,当然,也有一些对象不再被引用了,则将被回收掉。
当年轻代填满时,会触发一次年轻代回收(a young generation collection,也叫a minor collection);当年老代或永久代填满时,会引发一次Full GC(也叫a major collection),Full GC将对年轻代、年老代、永久代全部进行GC。Full GC涉及的面广,回收时间长,对应用程序会有影响,比如程序卡顿现象,所以一般需要参数设置年轻代回收更高效,尽量避免Full GC。
占用一个CPU,对年轻代、年老代串行回收,回收时stop the world。
年轻代回收
如图:
在回收过程中,eden区中仍被引用的对象(the live objects)复制到空的survivor区(To区),因较大(占用内存空间大)而不能复制到To区的对象,则直接复制到年老代;存放有对象的survivor区(From区)中仍被引用的对象,已经过一定次数年轻代回收且有资格竞升的,则复制到年老代,不足次数的则复制到To区。需要注意的是:如果To区已经填满,eden区、From区中仍被引用但未复制到To区的对象则直接复制到年老代,不管经过几次年轻代回收。对eden区、From区中不再被引用的对象(图中用红叉表示),回收器不进行标记,将被当作garbage回收。当前回收完成后,eden区、From区清空,只有To区有对象,此时,From区和To区角色互换,以为下一次回收做准备,如图:
年老代回收
采用标记—清理—压缩(mark-sweep-compact)算法(永久代回收也采用该算法)。在标记阶段,回收器标记哪些对象仍被引用;在清理阶段,扫描年老代,识别哪些对象不再被引用,且对其进行清理即回收其内存;在压缩阶段,回收器把仍被引用的对象统一往起始内存方向“滑动”,另一端则为连续可用的内存区,注意:这里的压缩不是内存大小意义上的,而是把存活对象统一“滑动”到一边,下同。另外,HotSpot在永久代回收方式和年老代一样,如果涉及压缩,则各自压缩,下同。这样,后续内存分配就可以采用“bump-the-pointer”技术快速分配。
快速分配(Fast Allocation):
“bump-the-pointer”技术:在很多情况下,堆中或方法区中会存在未使用的大连续内存块(large contiguous blocks),这样就可以采用“bump-the-pointer”技术给对象快速分配内存,即,回收器会记录上一次分配给对象内存的末尾地址,当给新对象分配时,回收器首先检查该地址后是否有足够连续的内存空间可供,如果有的话,则更新用于内存分配的指针(即记录该对象的末尾内存地址),后续初始化对象。
对多线程应用,内存分配需保证线程安全。如果采用全局锁(global locks),内存分配会成为应用程序的瓶颈,影响其性能。HotSpot 采用TLABs(Thread-Local Allocation Buffers)技术,通过给每个线程提供一个私有缓存(占用内存空间小),在该缓存中给对象分配内存,来提高多线程内存分配的吞吐量。由于私有该缓存,其他线程不会使用,在不加锁情况下,与“bump-the-pointer”技术配合使用,高效分配内存。当然,当私有缓存填满时,线程就需要同步另外获取一个,但这种情况比较罕见。当然,由于TLABs技术会占用一定内存空间,一些回收器会限制缓存大小,以免浪费内存,一般限制使用少于1%的eden大小。
适用场景
适用于客户端类型机器(简单而言就是机器配置低,后续会说明什么配置属于这种类型),对暂停时间要求宽松。当前计算机硬件条件下,串行回收器可较好管理64MB堆,Full GC的暂停时间控制在500ms以内。
参数设置
J2SE 5.0环境下,在客户端类型机器上,串行回收器为HotSpot的默认回收器,也可用-XX:+UseSerialGC参数明确选定。
又称吞吐量回收器(the throughput collector),主要是为了充分利用当前硬件多CPU优势,以提高回收效率。
年轻代回收
串行回收器年轻代回收的并行版本,仍然复制对象,stop-the-world,关键是利用多CPU并行回收,降低GC开销,从而增加应用吞吐量。下图为串行回收器与并行回收器在年轻代回收的区别:
并行回收器占用多CPU,启动多个回收线程,并行回收,而串行回收器只占用一个CPU串行回收;其stop-the-world的时间更短。
年老代回收
和串行回收器一样,采用标记—清理—压缩算法,串行回收(注意:年老代不是并行回收)。
适用场景
适用于多CPU优势的机器,对暂停时间没有要求,例如一些专门做批处理、科学计算等的应用。会较低频率地发生Full GC。
参数设置
在J2SE 5.0版本中,在服务器类型机器(简单而言就是机器配置较高,后续会说明什么配置属于这种类型)上,并行回收器为HotSpot的默认回收器,也可设置-XX:+UseParallelGC明确选定。
并行回收器的替代品,对年老代,采用新算法并行回收。
年轻代回收
与并行回收器一样:串行回收器年轻代回收的并行版本,仍然复制对象,stop-the-world。
年老代回收
简单而言采用并行回收、压缩。
首先,年老代在逻辑上被划分成固定大小的不同区域。分三个阶段进行:
1)在标记阶段,利用多CPU优势,开启多个回收线程,通过扫描应用代码查看哪些对象被直接引用的方式,并行进行初始标记。再根据区域的划分,对被标记为仍被引用的对象,记录所在地址、占用内存大小;
2)在分析阶段(the summary phase),主要针对区域而不是对象。由于上一次回收所进行的压缩,靠近年老代起始内存的方向,会密集大部分存活的对象,在这些区域中进行压缩的效果将不明显,因此,首先要确定哪些区域值得去压缩。从起始区域开始,往末尾区域方向分析,直到某个位置,达到一个分析值,在该位置往后,这些区域值得去压缩,往前的那些区域被称为“密度前缀”(dense prefix),不进行压缩。再计算和记录待压缩区域中所有存活对象待“滑动”的新起始地址。需要注意的是,分析阶段当前仍采用串行方式实现,并行方式也可以,只不过相对于标记、压缩阶段,其性能提升效果不明显。
3)在压缩阶段,利用分析阶段的记录,用多个回收线程并行地将对象复制到已分析好的区域的空白内存。这样下来,年老代一端会密集存活对象,另一端则会有较大连续空白内存可用。
适用场景
适用于多CPU优势的机器,与并行回收器相比,暂停时间更短,更适用于对暂停时间有要求的应用。
在一些配置好的共享机器(large shared machines)上,例如SunRays,则不太适用,因为这些机器上的应用被要求不能独占多个CPU的几个周期时间段,机器的主要作用是共享,如果非要勉强用并行压缩回收器,则只能用–XX:ParallelGCThreads=n来设置并行回收的线程数,少用回收线程。
参数设置
可设置-XX:+UseParallelOldGC明确指定。
对很多应用来说,相对于吞吐量,更关注于快速响应时间。一般来说,年轻代回收暂停时间短,但年老代回收会较低频率地出现长暂停,特别是占用内存很大的时候。为了针对解决年老代回收长暂停的问题,HotSpot设计了并发标记-清理回收器,又称低时延回收器(low-latency collector)。
年轻代回收
与并行回收器一样:串行回收器年轻代回收的并行版本,仍然复制对象,stop-the-world。
年老代回收
并发标记—清理回收器的回收工作大部分时间与应用程序是并发(注意:这里是并发,当然也会用多个线程并行回收)进行的。分两个阶段进行:
1)在标记阶段:又细分三个阶段,在初始标记(the initial mark)阶段(应用会短暂停),通过扫描应用代码查看哪些对象被直接引用的方式,进行初始标记;在并发标记阶段(the concurrent marking phase),标记那些因初始阶段中的直接引用而间接被引用的对象。由于标记与应用程序并发进行,应用会动态更新标识过引用的状态,所以不能保证所有存活的对象都已标记;在重标记(remark)阶段(应用再次短暂停),通过对上一阶段中应用程序修改过的对象再次访问,以确定标记结果。由于重标记比初始标记重要得多,所以会用多个线程并行进行,以提高效率。
2)在并发清理阶段(concurrent sweep phase):并发清理所有已识别的garbage。
下图为在年老代回收中串行的标记—清理—压缩回收器与CMS回收器的区别:
从上图可知:
1)CMS回收器在初始标记和重标记阶段会stop-the-world,初始标记用一个线程,重标记用多个线程并发标记,而串行的标记—清理—压缩回收器只用一个线程串行完成标记、清理、压缩工作,CMS回收器总的暂停时间比串行的要短,且在暂停时间段中的工作效率更高;
2)CMS回收器中的并发主要体现在两点:在并发标记阶段,用一个线程与应用并发标记;在并发清理阶段,用一个线程与应用并发清理。
但CMS回收器为了达到低时延特性是以以下开销为代价的:
1)在重标记阶段,对对象的重访问增加了回收器的工作量,这是典型的为了低时延而付出的权衡开销;
2)存在内存碎片,内存分配开销增加:CMS回收器是唯一不压缩的回收器,虽然省下了压缩时间,但年老代中回收后的可用内存不连续分布,存在内存碎片,回收器就不能利用 “bump-the-pointer”技术简单、高效地为新对象分配内存:
由于存在内存碎片,需要借助可用内存链表,假如有新的对象需要内存,CMS回收器根据申请的内存大小,在可用内存链表中查询,选择最合适(不大不小)的可用内存分配给新对象,这样就增加了分配开销。这增加了年轻代回收的额外开销,因为大部分发生在年老代的内存分配是年轻代回收中对象竞升引起的。
针对内存碎片的解决方法:CMS回收器会监测普通对象大小,估计内存分配需求,通过分解或合并可用内存的方式来满足需求。
3)需要更大的堆内存:因为在并发标记阶段,应用仍在运行,应用可能为创建新对象而分配内存,这会增加年老代的内存大小。
4)存在浮动garbage(floating garbage):在标记阶段,虽然能保证所有存活的对象都被标记,但是有些对象可能不再被引用而成为garbage,这些对象只能下次回收掉,被称为浮动garbage。
与其他回收器不同,CMS回收器不会等到年老代填满时才进行Full GC,而是提前启动Full GC,在填满之前完成回收;否则,切换采用标记—清理—压缩算法串行回收。为尽量避免采用串行回收,CMS回收器在启动阶段,会对之前回收所需时间、填满所需时间进行统计分析,以计算提前启动Full GC时间点。也可以用–XX:CMSInitiatingOccupancyFraction=n(其中n为年老代占用百分比,默认为68)参数设置初始占有率(the initiating occupancy),若年老代已用内存占有率超过该值,则提前启动Full GC。
总的来说,与并行回收器相比,CMS回收器降低了Full GC的暂停时间(有的时候降低幅度很大),这是以付出稍长的年轻代回收暂停、减小应用吞吐量、更大的堆内存为代价。
增量模式(Incremental Mode)
通过周期性地停止并发回收工作,把更多的CPU占用时间段还给应用程序,以降低长时间的并发回收对其的影响。这些周期性停止的并发工作,被划分为一串串小片段,分配在年轻代回收中计划进行。这种工作方式适用于:应用程序在低配置(CPU数少,一个或两个)的机器上运行、采用CMS回收器回收,且要求低时延。
适用场景
适用于多CPU优势的机器,有低时延要求,应用程序运行时有能力提供CPU资源给回收器,需要更大的堆内存空间。例如,Web服务器、运行在单CPU上带有稍大堆内存空间的交互性应用等。
参数设置
可设置-XX:+UseConcMarkSweepGC明确指定CMS回收器,增加设置–XX:+CMSIncrementalMode参数指定CMS回收器工作于增量模式。
Ergonomics是JVM根据所在的硬件平台、操作系统环境,选择默认工作模式、GC回收器、设置默认堆大小,以及通过设置默认GC回收器的性能指标自动调整堆中不同区域的大小以达到性能要求。期望在减少命令行参数下,JVM有良好的工作性能,满足应用要求。
服务器类型机器配置要求:1)2个及以上物理CPU;2)2G及以上物理内存。该要求适用于任何平台,除了在32位机器上运行Windows系统。在该类型机器,HotSpot默认工作于服务器模式(可用-client明确选择客户端工作模式),默认采用并行回收器。
其他都是客户端类型机器,JVM默认参数:1)采用客户端工作模式;2)采用串行回收器;3)堆初始值为4MB;4)堆最大值为64MB。
在服务器类型机器上,选择并行回收器的JVM(客户端模式或服务器模式),其堆大小默认值:1)初始值为内存的1/64,最大到1GB(注意,最小的初始值是32MB,因为服务器类型机器至少有2GB的内存);最大值为内存的1/4,最大到1GB。否则,采用客户端类型机器上的默认值。
可明确设置相关参数而不采用默认值。
在J2SE 5.0中,对并行回收器,增加了基于行为的调优方式。通过设置最大暂停时间、最大应用吞吐量性能指标,JVM会自动调整其行为以满足性能要求。
最大暂停时间目标
可用-XX:MaxGCPauseMillis=n参数设置,其中n表示最大暂停时间,单位为毫秒。并行回收器通过调整堆大小及其他参数,以使暂停时间小于n毫秒。这种方式可能会减少应用总吞吐量,另外目标也不一定达到。该目标是分别针对堆中不同区域(年轻代、年老代),若区域未达到目标,可能会减少其大小,以达到要求。
吞吐量目标
可通过-XX:GCTimeRatio=n参数设置,GC占用时间与应用的比值为:1/(1 + n),例如,设置-XX:GCTimeRatio=19,则表明5%的时间用于GC。n默认为99即GC占用时间为1%。如果没达到目标,回收器会相应增加堆中区域大小,以增加应用占用时间。
堆大小目标
如果达到了最大暂停时间、吞吐量,回收器会减小堆大小,直到其中一个指标没有达到,之后又调整以满足。
目标调整优先级
并行回收器首先满足最大暂停时间,然后才满足吞吐量,最后才会去满足堆大小。
实际上,HotSpot的Ergonomics可以满足大部分应用的性能要求。
建议:首先,让HotSpot,根据应用所处的硬件环境、操作系统,基于Ergonomics去自动选择回收器、工作模式以及堆大小。测试GC性能,若能满足要求(例如,有较高的应用吞吐量、较低的暂停时间),就不需要去调优;否则,根据测试结果、应用的性能要求亮点、平台特性,思考默认设置哪些不利于所要求的性能,例如,是不是回收器不合适等等。再设置合适的GC参数,如选择GC回收器、设置堆大小等,以达到性能要求。
关于堆大小
1)堆的总内存是否够用是影响GC性能的最重要因素:除非暂停时间未达到要求,一般设置堆相对大,这样吞吐量会受益;
2)第二个影响GC性能最重要的因素是年轻代在堆中所占的比重:除非年老代过度回收或暂停时间未达到要求,一般需要给年轻代一定大的内存空间,当然,如果选用串行回收器,年轻代不能超过堆大小的一半。
关于并行回收器、并行压缩回收器调优
当使用并行回收器或并行压缩回收器,设置应用吞吐量目标,无需设置堆最大值,除非明确需要比默认的堆最大值更大,回收器会动态调整堆中区域及大小,以达到要求。若堆已到最大值,还未达到吞吐量目标,则设置堆最大值以接近物理内存大小(当然不能对系统及应用运行有破坏影响),再测试,若仍未达到目标,则表明目标值对当前硬件条件太高。
若满足吞吐量目标,但暂停时间太长,则设置最大暂停时间目标,这意味着吞吐量目标不一定满足。达到吞吐量目标,需要较大的堆;而达到最大暂停时间目标、最小堆目标,需要较小的堆,需要折中。
1)首先真实测试,再调优,不要为了调优而调优;
2)避免调优过时:在某HotSpot调优后,若应用有修改或对性能要求有变、硬件环境有变、甚至回收器的实现改变等,需要重新调优;
3)当使用并行回收器或并行压缩回收器,推荐设置性能目标,而不是明确堆大小(eden区、survivor区、年轻代、年老代),回收器会动态调整堆中区域及大小,以达到要求。
1)设置–XX:+PrintGCDetails参数:输出关于每次年轻代、年老代、永久代回收前后被引用对象的大小,每个区域可用内存大小,以及回收时长;
2)设置–XX:+PrintGCTimeStamps:与–XX:+PrintGCDetails配合使用,输出回收开始时间,用于了解GC回收情况;
3)jmap:一个JDK中的命令行工具,输出JVM及core文件的内存统计信息。a)带–heap选项,输出回收器名称、回收器使用的具体算法细节(例如,并行回收器使用的线程数)、堆配置信息、堆使用率统计;b)带–histo参数,输出堆中类对象直方图数据,对每一个类,输出堆中实例个数、这些实例所占用的总内存大小以及全限定名称。这便于了解堆是如何使用的;c)带–permstat选项,输出永久代中对象的统计信息。
4)jstat:一个JDK中的命令行工具,输出性能、资源消费情况。该工具用于诊断性能问题,特别是那些关于堆大小、GC的。其中参数可用于输出GC行为、内存容量、年轻代\年老代\永久代的使用率等统计信息。
5)HPROF:堆探查器(Heap Profiler),JDK中内置的一个简单插件。它是一个动态链接库,使用JVM工具接口(Java Virtual Machine Tools Interface:JVM TI)给JVM实现提供接口。输出CPU使用率、堆分配统计信息、监测资源竞争情况,另外,可导出完整堆,可输出JVM中所有监控锁和线程的状态,可将信息以文件形式、二进制形式输出,或以ASCll形式传给一个套接字(socket)。HPROF适用于分析性能、锁竞争、内存泄漏等问题。
6)HAT(Heap Analysis Tool):用于分析不需要的对象因有引用指向而不能回收等现象,可查看HPROF输出的堆快照中对象的引用拓扑,可多种方式显示,例如从根对象到当前对象的引用路径。
GC回收器选择
GC回收器统计信息
堆和区域大小
关于并行回收器、并行压缩回收器的选项
关于并发标记—清理回收器的选项