内存管理的方式
内存管理有显式内存管理和隐式内存管理(交给垃圾收集器处理)两种方式。
显式内存管理会带来两个问题,一是悬空引用(一个对象仍然被其他对象引用着,但它的空间却被回收、重新分配了),另一个是空间泄漏(对象已经不再被引用了,但分配给它的内存没被释放)。
如果交给垃圾收集器来进行内存管理,垃圾收集器就需要负责:
- 分配内存
- 保证被引用的对象都存在在内存中
- 对象不可达时,回收它占用的内存
Java采用的是隐式内存管理,JVM会提供垃圾收集器完成内存的分配和回收。
垃圾收集器
垃圾收集器在工作过程中,要么和应用程序并行执行,收集垃圾的时候不影响应用程序的运行,要么就和应用程序顺序执行,中断应用程序(stop-the-world)进行垃圾收集。对于收集之后产生的内存碎片,有压缩(将live对象移到一起)、不压缩(速度快,但产生碎片)和拷贝(将live对象拷贝到不同的内存区域)三种方式。
而对垃圾收集器整体来说,除了前面所说的需求和工作方式外,还要考虑一些性能指标:
- 吞吐量——非垃圾回收时间所占的百分比
- 垃圾回收开销——垃圾回收时间所占的百分比
- 暂停时间——停止应用、执行垃圾收集的时间
- 收集频率
- footprint——度量收集回来的内存大小
- 及时性——对象成为垃圾之后,到占用的内存变得可用所需的时间
HotSpot JVM里的垃圾收集器( Java SE 7.0 之前)
HotSpot JVM将内存划分为年轻代、老年代和永久代。年轻代——大部分对象初始化时都分配到此区域,里面包含一个eden区(对象初始分配在eden里)和两个survivor区(一个保持空,另一个存放存活下来的对象)。老年代——一些大对象初始化时会分配到此区域;年轻代收集若干次之后存活的对象也会提升到老年代中。永久代——存放描述类和方法的对象;还有类和方法本身。
下面看看HotSpot JVM里的垃圾收集器。
顺序收集器
顾名思义是顺序执行的,所以会stop-the-world,适用于对暂停时间要求不高的应用。在Java里,顺序收集器是非服务器级别机器上缺省的垃圾收集器。可以使用-XX:+UseSerialGC指定。
顺序收集器在年轻代的行为
eden区域的live对象拷贝到空的survivor空间(To),相对survivor来说较大的live对象会直接拷贝到老年代。
另一个survivor空间(From)里的live年轻对象也会拷贝到To survivor空间,相对老的live对象会拷贝到老年代。
如果To survivor空间满了,eden和From survivor里的live对象就会拷贝到老年代去。
最终,eden区域和From区域会被清空。两个survivor区域互换角色。
顺序收集器在老年代的行为
老年代和永久代都使用mark-sweep-compact算法。
标记阶段,收集器辨别仍然活着的对象。
清理阶段,清理代中的“垃圾”。
移动压缩阶段,将live对象移动到老年代空间的开始处(永久代也一样),留出后面连续的空闲空间。之后的分配使用bump-the-pointer技术,会比较快速。
并行收集器(吞吐量收集器)
并行收集器是服务器级别机器上缺省的垃圾收集器。可以使用-XX:+UseParallelGC指定。
并行收集器在年轻代的行为
算法是顺序收集器在年轻代的算法的并行版本,仍然是stop-the-world和拷贝。
并行收集器在老年代的行为
算法仍然是mark-sweep-compact。
从并行收集器的算法可以看出,它适用于在多核机器上运行、但对暂停时间的要求也不高的应用。常见的有批处理、科学计算、计费类应用。
并行压缩收集器
并行压缩收集器从Java 5.0的update 6开始引入。可以使用-XX:+UseParallelOldGC指定。
和并行收集器的区别在于老年代垃圾回收的算法。老年代和永久代都用stop-the-world和移动压缩,不同在于是并行的。
- 首先每个代逻辑划分成固定大小的区域。在标记阶段,对象会分配给垃圾收集线程,并行标记live对象。一个对象标记为live对象后,会记录对象的大小和位置。
- summary阶段对区域进行操作,而不是对象。因为前一次收集压缩之后,live对象往往集中在代的左端,此时对密度比较大的区域进行压缩会不太值得。所以,summary阶段首先是从最左端检查各个区域的密度,直到找到一个能恢复的、而且它右边的区域值得压缩的点。这个点左边的区域叫做“dense prefix”(密度前缀),不会被移动这些区域里的对象。这个点右边的区域会被压缩。对于每个被压缩过的区域,summary阶段会计算并存储里面live对象第一个字节的新位置。需要注意的是,目前的summary阶段还是顺序的,并行化带来的性能改进不及标记和压缩阶段的并行化。
- 压缩阶段,垃圾收集线程使用summary数据找出需要填充的区域,各个线程并行把数据拷贝到那些区域中。这样处理之后,堆的一头密度就会较大,另一头则会有空的区域。
并行压缩收集器的适用场景和并行收集器类似,但如果对暂停时间的要求高一些的话,并行压缩收集器要比并行收集器好。可以使用–XX:ParallelGCThreads=n指定线程数。
CMS(concurrent mark-sweep)收集器
CMS收集器也叫低延迟收集器,能减少暂停的时间,提升应用的响应时间。可以使用–XX:+UseConcMarkSweepGC指定。
CMS收集器在年轻代的行为
收集和并行收集器在年轻代的行为一样。
CMS收集器在老年代的行为
大部分收集和应用的执行是并行的。
CMS收集器的收集周期始于一个短暂的暂停,叫做“initial mark”,找出应用代码直接可达的live对象初始集合。接着是并行标记阶段,标记通过初始集合能到达的所有live对象。由于应用在运行,收集器标记的同时引用情况会更新,所以并行标记阶段结束的时候,并不能保证所有的live对象都被标记到。为了解决这个问题,应用会再次暂停一下,叫做“remark”,完成live对象的最终标记。remark暂停比initial mark的暂停时间长,所以会用多线程并发执行,来提升效率。
接着进入并行sweep阶段,基于remark阶段的标记,清除掉所有垃圾。下图是顺序mark-sweep-compact收集器和CMS收集器在老年代收集上的区别。
利弊分析
CMS收集器是唯一不进行压缩的收集器。释放垃圾占用的空间后,不会把live对象移到老年代的一头。这会节省GC的时间,但会有碎片,而且由于没有连续的空闲空间,收集器在给进入老年代的对象分配内存时会比较昂贵。老年代主要是给年轻代收集之后晋升上来的对象分配空间,所以不压缩也会给年轻代带来一些额外的开销。
和其他收集器相比,CMS收集器的另一个缺陷是需要较大的堆空间。标记阶段应用还在运行,所以会继续分配内存,老年代就会继续增长。另外,标记阶段虽然会识别出所有的live对象,但其中一些对象可能会在标记阶段变成垃圾,下一次老年代收集时才会收集这些对象。
CMS收集器还有一个独特之处,老年代的收集动作并不是在老年代满的时候开始,而是会尽早进行。要是老年代满了才开始收集,CMS收集器会切换成stop-the-world的mark-sweep-compact算法。为了避免这种情况,CMS收集器会根据先前的收集次数和老年代消耗的速度决定收集开始的时间。另外,也可以在命令行设置–XX:CMSInitiatingOccupancyFraction=n选项,其中n表示老年代的初始占用率(百分比),一旦超过n,CMS收集器就会开始收集。n的缺省值是68。
总的来说,CMS收集器能减少老年代暂停,但会略微增加年轻代的暂停时间,损失一点点吞吐量,而且对堆大小的要求会更高一些。
增量模式
CMS收集器的并行阶段可以增量进行,就是阶段性地停止并行阶段,将处理器让给应用。应用如果运行在处理器较少(比如1或2个)的机器上,要求暂停时间比较短,就可以选择增量模式。
如果应用要求垃圾收集暂停时间比较短,又能在运行时和垃圾收集器共享处理器资源,就可以使用CMS收集器。应用里长期存活的数据要是相对比较多,而且应用运行的机器有两个或多个处理器,应用采用CMS收集器就比较好,比如Web服务器。
Ergonomics——自动选择和调优
从Java 5.0起,JVM能根据应用运行的平台和操作系统自动选择垃圾收集器、堆大小、以及HotSpot虚拟机类型(client还是server),能更好地满足不同类型应用的需求,减少命令行选项的设置。此外,并行垃圾收集器也能动态调优。根据平台自动选择和垃圾收集调优综合起来就是Ergonomics。Ergonomics的目标就是减少命令行参数,但提升JVM的性能。
垃圾收集器、堆大小和虚拟机的自动选择
服务器级别的机器指具备两个或多个物理处理器、2G及以上物理内存的非32位Windows平台。
对非服务器级别的机器来说,JVM、垃圾收集器、堆大小的缺省值是:
- client JVM
- 顺序垃圾收集器
- 初始堆大小是4MB,最大堆大小是64MB
对Server级别的机器来说,缺省值是:
- server JVM(除非显式指定-client命令行选项)
- server JVM选择并行垃圾收集器,client JVM选择顺序垃圾收集器
- 垃圾收集器如果是并行垃圾收集器,初始堆大小是物理内存的1/64(上限1GB),最大堆大小是物理内存的1/4(上限是1GB)。其他情况下初始堆大小是4MB,最大堆大小是64MB
并行收集器调优
从Java SE 5.0开始,并行垃圾收集器能自动调优,以满足应用在命令行指定的最大暂停时间和吞吐量。
最大暂停时间
使用命令行选项-XX:MaxGCPauseMillis=n(毫秒)指定并行收集器的最大暂停时间。
并行收集器会根据堆大小和其他垃圾收集相关的参数进行调整,确保暂停时间小于n毫秒。不过这种调整可能会降低应用总的吞吐量,有些情况下也可能满足不了需求。
最大暂停时间对每个代是单独生效的,满足不了需求的时候,会减小代的大小。缺省没有暂停时间的限定。
吞吐量
吞吐量使用命令行选项-XX:GCTimeRatio=n来指定。垃圾收集花费的时间和应用运行的时间之间的比率就是1/(1+n)。n的缺省值是99,也就是垃圾收集花费的时间占总时间的1%。
垃圾收集花费的时间适用于所有代。满足不了需求的时候,会增加代的大小。
Footprint
如果吞吐量和最大暂停时间都满足了,垃圾收集器会减小堆的大小,直到满足不了其中一个目标(必然是吞吐量)。然后垃圾收集器又会进行调整,以达到指定的目标。
目标优先级
并行垃圾收集器首先会满足最大暂停时间,达到目标后才会调整去满足吞吐量目录。最大暂停时间和吞吐量都满足后才会考虑Footprint。
建议
Ergonomics技术会自动选择垃圾收集器、虚拟机和堆大小。所以在做应用调优时,先不要自己显式指定垃圾收集器等设置,先让系统根据应用所在的平台和OS进行自动选择,然后进行测试,垃圾收集相关的性能结果不好再调整垃圾收集器参数。然后持续进行“测试--调优”的过程。
分析性能的工具和常用选项将在后面介绍。