HotSpot虚拟机内存管理

第一章 简介

J2SE平台的一大优势是它的自动化内存管理,避免了开发者去面对内存管理的复杂性。

本文以Sun J2SE 5.0的发行版为例提供对HotSpot虚拟机的内存管理的一个宽泛的综述。文章描述了用于内存管理的GC(Garbage Collector),并对如何选择和配置GC,如何配置GC处理的内存区域大小等给出建议。本文也可以作为一种资源,其中列举了大量常用的影响GC行为的选项,以及一些索引到更详细文档的链接。

第二章节主要面向对自动内存管理概念不太了解的新人。其中包含一个简短的论述,关于自动内存管理和程序员手动内存管理的利弊。

第三章会呈现一个对GC的概念、设计选择、执行指标的综述,也会介绍一种常用的基于类的生存时间将内存划分为不同区域的组织方法,被称为“分代”。分代的隔离性已经证明其在降低GC暂停时间和应用整体耗时方面的高效。

文章剩余部分提供对HotSpot虚拟机信息的说明。第四章描述4种可用的GC,其中一个是在J2SE 5.0第六次更新中新引入的,同时整理了所有GC都使用的“分代”内存组织方法。第四章对每一种已经使用的回收算法的类型进行总结以及说明应该如何选择合适的GC。

第五章描述了一种J2SE 5.0新的技术,综合了动态GC选择,堆管理,基于应用运行平台和操作系统的HotSpot虚拟机,根据用户期望行为的GC动态地切换等多个方面。这项技术被认为是工效学的应用。

第六章提供选择和配置GC的建议,以及处理OOM(Out Of Memory)的忠告。第七章简要的描述了一些可用于评估GC能效的工具。第八章列举了常用的命令行工具,用于GC的选择和行为。最后,第九章提供了一些链接,索引到本文覆盖一些主题的更详细的材料。

第二章 直接内存管理与自动内存管理

内存管理就是一个识别的过程,识别已分配内存的类何时不在需要,何时释放其占用的内存,以及何时为后续的内存分配标记其是可用的。在某些语言中,内存管理是程序员的责任。其复杂性会导致许多常见的错误,引起难以预料的程序行为甚至崩溃。正因为如此,开发者会花费大量的时间调试和修复这类错误。

直接内存管理会发生一种常见的问题是“悬挂引用”。给一个类分配一块另一些类仍然在使用的内存区域是很有可能的。如果悬挂引用的类尝试访问类实例,而其空间已经分配给新的类了,那么结果将不可预料,也不知道将会发生什么。

另一个常见的直接内存管理的问题是“内存泄漏”。如果已经分配的内存不在被引用,而又没有释放,内存泄漏就会发生。举例来说,你打算给释放一个链表的内存空间,然而在访问第一个元素的释放,发生了错误,那么剩余的链表元素也就不在被引用,但它们已经不能被程序访问,也不能被使用或者覆盖。如果内存泄漏发生的足够多,那么它们会持续侵占内存,直到内存耗尽。

一种内存管理的替代方案就是利用具有自动化内存管理特性的GC程序,其被大量现代面向对象的编程语言所采用。自动化的内存管理提升了接口的抽象能力和代码的可靠性。

垃圾回收器避免了悬挂引用的问题,因为正在使用得类是不会被当做垃圾回收的,也就更不会被释放。垃圾回收器也能解决内存泄漏的问题,因为它会释放掉所有不在使用的内存区域。

第三章 垃圾回收器的概念

垃圾回收器的工作是:

  1. 分配内存
  2. 确保任何有引用的类都是在内存中
  3. 收回代码中不再使用的类所占用的内存区域

存在引用的类,通常会说成是“存活的”。不再被引用的类会被认为是“死的”,术语称作是垃圾。发现和释放那些内存区域的程序,被称作垃圾回收器。

垃圾回收器可以解决很多,但不是所有的内存分配问题。举例来说,你可以无限制的创建类,持续的引用它们,直到内存耗尽。垃圾回收器本身也是一个耗费时间和资源的任务。

垃圾回收器采用精准的算法来识别、分配和释放内存,并隐藏起来不被程序员发现。分配的内存空间通常来自于一个被称为“堆”的内存池中。

垃圾回收的耗时取决于垃圾回收器本身。典型的情况是,当堆满了或者达到一定的占用比例阈值,整个堆或者一部分会被回收。
完成内存分配请求的任务,涉及到查询一块特定大小而又可用的内存区域,是比较困难的。动态内存分配的主要问题是避免碎片化,同时保持高效的内存分配和释放。

期望的垃圾回收特性

垃圾回收器必须是安全和全面的。存活的数据绝对不能被释放,而垃圾不应该在多次回收循环后仍然停留。

垃圾回收器处理高效,没有引入长时间的暂停,因为暂停期间应用是不能运行的。然而,像大多数类计算机系统,通常需要在时间、空间、频率上进行折中。举例来说,假如堆很小,回收就会更快,但是堆也会更快的填满,也就需要更快的回收频率。相反,一个大的堆将花费更久的时间填满内存,同时回收的频率也就更慢,但是回收也就要消耗更多时间。

另个一个期望的特性是限制碎片的数量。当垃圾类的内存被回收,释放的空间可能会零散的分布在多个区域,那么就可能没有足够的内存为一个大对象分配一个连续的区域。消除碎片的一个方案被称为“压缩”,在下面的不同垃圾回收器的设计选择中将会讨论。

扩展性也很重要。对于多核系统上的多线程环境,内存分配不应该称为瓶颈,同理回收也是如此。

设计选择

当设计和选择一个垃圾回收算法时,需要作出如下的抉择:

  • 串行 vs 并行
    使用串行回收器时,任何时候都只能有一样东西在运行。举例来说,即使有多个CPU可以使用,也只能有一个用于垃圾回收。在使用并行垃圾回收器时,回收任务会被拆分为多个子任务,不同的子任务可以同时在多个CPU上进行处理。并行处理使得回收过程完成的更加快速,代价就是更加复杂且有潜在的碎片。
  • 并发 vs 停止等待
    当停止等待垃圾回收器在运行时,应用程序将会被暂停。可代替的方案是由一个或者多个垃圾回收任务能够并发执行,跟应用同时运行。更典型的情况是,一个并发垃圾回收器处理大部分任务都是并发的,但是偶尔也不得不采用一个短暂停止等待的暂停。停止等待回收器相较于并发垃圾回收器要简单,因为堆被冻结,回收期间类也不会再改变。它的劣势是应用的暂停是人们所不期望的。相对应的,并发回收垃圾,暂停时间会更短,但是垃圾回收器必须更加小心,因为它正在处理的类,可能正在被应用更新。对并发回收器来说,这会增加一些开销,影响回收器的执行效率同时也要求有更大的堆。
  • 压缩 vs 非压缩 vs 复制
    在垃圾回收器决定哪些类是存活的,哪些是垃圾之后,他可以压缩这部分空间,移动所有存活的类到另一块完整的区域里,然后重新声明剩余的内存。压缩之后,在第一步释放的区域中分配新的类会更加容易。一个简单的指针可以跟踪下一个可以被分配的内存区域。与压缩回收器相比,非压缩回收器会就地释放被占用的空间,它不会像压缩回收器那样移动存活的类而创造一个更大的内存区域。好处是更快,缺点是碎片化。一般来说,在一个就地回收的堆中比在一个压缩的堆中分配内存代价更高。搜索一块足够大的能够容纳新类的连续区域是必须的。第三种选择是,拷贝回收,拷贝存活的类到不同的内存区域。优点是源区是空的,可以快速和容易的进行内存的分配。缺点是需要消耗复制的时间,以及额外的内存区域。
能效指标

有很多指标可以衡量垃圾回收器的运行能效,包括:

  • 吞吐量-在一个长的时间段内,没有花在垃圾回收上的时间占整个时间周期的比例
  • 垃圾回收隐形消耗-与吞吐量相反,花在垃圾回收上的时间占整个时间周期的比例
  • 暂停时间-垃圾回收正在运行,应用停止的时间
  • 回收频率-垃圾回收的频率
  • 覆盖区域-内存的测量,如堆内存
  • 迅速-在成为垃圾到回收掉垃圾之间的时间消耗

一个交互式应用可能要求低的暂停时间,而对于非交互式应用,整个的执行时间更加重要。一个实时的应用希望在任何阶段垃圾回收暂停时间和回收的时间都有一个较小的上界。对于个人计算机或者集成式系统,使用小的内存是最主要的关注点。

分代垃圾回收

当一个被称为分代的技术被使用时,内存区域将会被分成不同的带,存储不同年段的类。举例来说,最广泛采用的分代配置方法是:年轻代和老年代。

不同的带中会采用不同的垃圾回收算法,每种算法会根据不同带的特性进行优化。分代回收具有以下特性,被称作“弱分代理论”,无论任何语言写的程序,包括java:

  • 大部分类都不会存活很久,它们会在年轻代消亡
  • 没有类是会从老年再变回年轻的

年轻代的回收会频繁发生,高效而迅速,因为年轻代通常较小且更有可能存储大量不再被引用的类。

在多次年轻代的回收中幸存的类,将会被提升到老年代。如图1,老年代通常会大于年轻代,它的容量也会增长的更慢。正因为如此,老年代的回收频率是比较低的,回收的时间也会比较长。

HotSpot虚拟机内存管理_第1张图片
图片 1.png

对于年轻代的垃圾回收算法更关注于速度,因为年轻代更加频繁。对于老年代的算法更关注于空间效率,因为老年代占用大部分的堆内存,其算法不得不处理好低垃圾密度的回收。

第四章 J2SE 5.0中HotSpot虚拟机的垃圾回收器

在J2SE 5.0的第6次更新中,HotSpot虚拟机包括4个垃圾回收器。所有的回收器都是分代的。本章节将描述分代机制和回收器类型,讨论为什么内存分配通常是快速而高效的,同时也会提供每种回收器的详细信息。

HotSpot分代

HotSpot虚拟机中内存被分为三个代:年轻代、老年代、永久代。大部分类初始化时都会被存储在年轻代(较大的类可以直接存储在老年代)。老年代存储着一些经过若干次年轻代的回收依然存活的对象,以及一些直接分配在老年代的较大对象。永久代存储一些JVM很方便进行垃圾管理的对象,如描述类和方法的对象,以及类和方法本身。

HotSpot虚拟机内存管理_第2张图片
图片 2.png

年轻代包含一个Eden区和两个Survivor区,如图2所示。大部分类初始化都被分配在Eden区。Survivor区存储着至少在一次年轻代回收中幸存的对象,它们在被提升到老年代之前还有一些额外的幸存机会。任何时候,Survivor区中都有一个存储着对象,另一个是空的,直到下一次回收。

垃圾回收类型

当年轻代填满,年轻代的回收算法(minor GC)将执行。当老年代或者永久代填满时,著名的full GC(major GC)将会执行,那时所有的分代将会被回收。通常,年轻代将会先回收,使用针对年轻代设计的回收算法,因为它通常在确定年轻代的垃圾上更加高效。然后是使用该回收器上针对老年代的回收算法,回收老年代和永久代。如果有压缩,那么所有代都会单独发生压缩。

有时,如果年轻代先回收,老年代又太满而不能接收来自年轻代向老年代提升的类。这种情况下,所有的回收器除了CMS,年轻代的回收算法将不在运行。代替的是老年代的算法将会用在整个堆上。(CMS的老年代回收算法是个特例,因为它不能回收年轻代)

快速分配内存

在下面对垃圾回收器的描述中,你将会看到在许多情况下会有大量的临近内存块可以用于内存分配。使用空闲指针的技术,在那些内存块上进行分配是非常高效的。上一个分配内存的对象的结束点将会被跟踪(内存被跟踪)。当一个新的分配请求需要满足时,所有被处理的请求都会检测代内剩余的空间是否能够满足需要,如果满足,就会更新指针,初始化类对象。

对于多线程的环境,分配操作需要是线程安全的。如果使用全局锁来进行保证,内存分配将变成一个瓶颈,降低运行的效率。代替,HotSpot虚拟机将采用一项新的技术,被称为TLAB(Thread-Local Allocation Buffers)。这项技术通过给每个线程分配一个自己的缓存池(代中很小的一部分),改进了多线程分配的吞吐量。因为在每个TLAB中仅有一个线程可以分配,分配通过利用空闲指针技术快速进行,不需要任何锁操作。仅在很低的频率下,如果一个线程填满了自己的TLAB,就必须进行同步操作。由于TLAB的使用,有多项缩小空间消耗的技术被应用。举例来说,分配器平均只给TLAB分配低于Eden区1%的内存。TLAB和使用空闲指针技术的线性分配混合使用,加强了分配的高效性,仅需要10个左右的原生指令。

Serial回收器

在Serial回收器中,年轻代和老年代都是串行回收的(使用单个CPU),以一种停止等待的方式。回收进行时,应用将会暂停。

  1. 年轻代回收会用Serial回收器
        如图3所示,年轻代使用Serial回收器。Eden区存活的对象将会复制到空闲的Survivor区,在图中被标记为To,不包括那些太大而不能放进To的对象。那些对象会被直接放到老年代。在From的Survivor区,仍然是相对年轻的对象,将被复制到To的Survivor区,而相对老的对象将会被复制到老年代。注意:如果说To区变满,那么Eden区和From区仍然存活的对象将被转移到老年代,无论它在多少轮的年轻代回收中幸存。在存活的对象被拷贝出去之后,按照规则来说,Eden区和From区剩余的所有对象都不是存活的,也不需要被检测。(X标记的是垃圾对象,实际上回收器并不会去检测和标记它们)


    HotSpot虚拟机内存管理_第3张图片
    图片 3.png

        在年轻代的回收完成之后,Eden和先前占用的Survivor区将会变成空的,仅有先前空的Survivor区保留一些存活的对象。某种意义上来说,Survivor区采用交换原则,如图4。


    HotSpot虚拟机内存管理_第4张图片
    图片 4.png
  2. 使用Serial回收器回收老年代
        在Serial回收器中,老年代和永久代的回收采用的是一种叫作mark-sweep-compact的算法。在标记阶段,回收器确认哪些对象仍然存活着。在交换阶段,对整个代的垃圾进行交换。然后,回收器会执行移动压缩,移动存活的对象到老年代的开始区域(永久代类似),所有剩余的连续空间被留在相反的区域。如图5所示。压缩允许使用快速的空闲指针技术在老年代和永久代中进行内存分配。

HotSpot虚拟机内存管理_第5张图片
图片 5.png
  1. 何时使用Serial回收器
        Serial回收器是大部分运行在client模式机器上的应用的选择,它们没有低暂停时间的需求。在当今的硬件环境下,Serial回收器高效的管理着许多重要的具有64MB堆的应用,相对于full回收,也减少了糟糕示例的暂停时间。

  2. Serial回收器选择
        在J2SE 5.0发行版中,Serial回收器在一些非Server模式下机器是作为默认的垃圾回收器,就像在第五章描述的那样。在另外一些机器上,Serial回收器要求一个严格的命令行参数-XX:+UseSerialGC。

并行回收器(Parallel Collector)

如今,许多Java应用都运行在大内存和多CPU的机器上。Parallel Collector又称为高吞吐回收器,其开发正是为了充分利用多CPU,避免使用一个核心进行垃圾回收时,其他的核心空闲。

  1. 年轻代使用并行回收器
        并行回收器在年轻代使用的回收算法是一种Serial回收器在年轻代使用算法的并行版本,是一种停止等待和复制的回收器,但是是以并行的方式在执行回收,使用多个CPU,降低垃圾回收的消耗,增加应用的吞吐量。如图6所示,Serial回收器和Parallel回收器的不同。
HotSpot虚拟机内存管理_第6张图片
图片 6.png
  1. 老年代使用并行回收器
        在并行回收器中,老年代的垃圾回收仍然使用串行回收器的串行mark-sweep-compact算法。

  2. 何时使用并行回收器
        能够收益于并行回收器的应用是那些可以运行在多CPU环境,且并没有暂停时间限制,老年代的回收仍然会发生,因为并不频繁,所以潜在可能要更长时间。并行回收器适用的应用是批处理,入账,工资计算,科学计算等。
        你可能会考虑运用并行压缩回收器到整个并行回收器中,因为前者可以在所有的代上执行并行回收,而不仅仅是在年轻代。

  3. 并行回收器的可选项
        在J2SE 5.0的发行版中,并行回收器是作为Server模式机器上的默认垃圾回收器。另一些机器中,需要声明-XX:+UseParallelGC命令才能使用并行回收器。

并行压缩回收器(Parallel Compacting Collector)

并行压缩回收器是在J2SE 5.0的第6次更新中提出的。与原并行回收器的不同在于采用了新的算法来处理老年代的垃圾回收。注意:实时上,并行压缩回收器将会取代并行回收器。

  1. 年轻代使用并行压缩回收器
        在并行压缩回收器中,年轻代垃圾回收使用的算法与并行回收器中使用的相同。

  2. 老年代使用并行压缩回收器
        在并行压缩回收器中,老年代和永久代是以一种暂停、并行的方式来移动压缩。回收器包括3各阶段。首先,每个代都会被分层固定大小的区块。在标记阶段,应用可以访问的存活对象集合被分割到不同的垃圾回收线程中,然后所有存活的类就会被并发标记。假如一个对象被认为是存活的,其所在区块的数据就会被更新,包括大小和对象的位置。
        汇总阶段要处理的是一个区块,而不是对象。因为先前回收的压缩,典型的情况是每个代左侧的部分包含较多存活对象,密度大。被如此高密度的区块覆盖的内存是不值得压缩的。汇总阶段首先要做的事情是检查区块的密度,从左侧的第一个块开始,直到达到某个临界点,在临界点右侧的被区块覆盖的内存是值得压缩的。临界点左侧的区域被认为是高密度的前缀,在那些区域没有对象需要移动。临界点的右侧需要被压缩,消除死的空间。汇总阶段会计算和存储每个压缩块存活数据块首字节的位置。注意:汇总阶段当前是串行执行的,并行化是可以的,但并不如在标记和压缩阶段那么重要。
        在压缩阶段,垃圾回收线程会使用汇总阶段的数据确定哪些区域需要被填充,然后各线程单并发向区域内拷贝数据。这会导致堆的一遍被高密度的堆积,另一边有一个大的空的内存块。

  3. 何时使用并行压缩回收器
        和并行回收器一样,并行压缩回收器对运行在多CPU的上应用是有好处的。另外,老年代的并行化操作降低了暂停时间,特别是对有暂停时间限制的应用,并行压缩回收器比并行回收器更合适。不过,并行压缩回收器对运行在大型共享内存机器上的应用不是很合适,因为没有某个应用可以独占若干个CPU超过一定的时段。在那些机器上,可以降低垃圾回收使用的线程数(通过设置-XX:ParallelGCThreads=n)或者选择不同垃圾回收器。

  4. 并行压缩回收器选择
        如果想使用并行压缩回收器,可以在声明命令行参数-XX:+UseParallelOldGC。

CMS回收器(Concurrent Mark-Sweep Collector)

对于许多应用,端到端的吞吐量并不如快速的响应时间来的重要。年轻代的回收通常不会导致长时间的暂停。但是,老年代的回收,尽管并不频繁,却会引起长时间的暂停,特别是使用大的堆时。为了解决这个问题,HotSpot虚拟机引入了一个称为CMS(Concurrent Mark-Sweep Collector,并发标记扫除回收器),以其低延时的特性而闻名。

  1. 年轻代使用CMS回收器
        CMS回收年轻代的方式同Parallel回收器相同。

  2. 老年代使用CMS回收器
        CMS在老年代的回收任务大部分是同应用的执行一起并发执行的。
    在CMS回收器回收循环的起始阶段,有一段短暂的暂停,称为初始化标记,用于确定应用直接可达的初始存活对象集。然后,在并发标记阶段,回收器标记所有的存活对象,这些对象是初始化对象集传递可达的。因为当标记是发生时,应用仍然在运行和更新引用,所以并不能保证所有存活的对象在标记结束时都能得到标记。为了处理这种情况,应用必须再次执行一个秒级的暂停,被称为再标记,再标记会通过重新访问所有在并发标记阶段更新的类来完成标记任务。因为再标记的暂停比起初始标记更大,因此通过多线程并发运行来提交其效率。
        在再标记结束时,所有对中存活对象是保证被标记,因此后续的并发扫除阶段会改造所有被确认的垃圾。如图7,说明使用串行Mark-Sweep-Compact回收器和CMS回收器在老年代的回收上的不同。


    HotSpot虚拟机内存管理_第7张图片
    图片 7.png

        因为一些任务,例如再标记阶段重新访问对象,会增加回收器的工作量,它的消耗也就会增加。对于大部分尝试减低暂停时间的回收器,这是一种典型的折中策略。
        CMS回收器是仅有的非压缩回收器,也就是在释放了死亡的对象所占用的空间后,并不会将存活对象移到老年代的一端,如图8。


    HotSpot虚拟机内存管理_第8张图片
    图片 8.png

        这节约了时间,但是因为释放的空间不是连续的,回收器不能再使用一个简单的指针来标记下一个空闲的可以用来给对象分配空间的内存位置。代替,需要采用一个空闲空间列表。未分配的空间通过一定数量的链表链接在一起,每一次为对象分配空间时,为了找到一个足够大能够容下对象的区域,必须搜索一个合适的链表。正因为如此,在老年代的内存分配上,相比使用空闲指针技术,代价更大。这也增加了年轻代回收上的额外消耗,因为大部分老年的分配是发生在年轻代回收中需要提升的对象上。
        CMS回收器另一个不利条件是要求更大相比其他回收器更大的堆空间。假如应用是被允许在在标记阶段运行时,它会继续分配内存,因此潜在地持续增加老年代。另外,回收器保证在标记阶段确认所有存活的对象,但是一些对象可能在标记阶段变为垃圾,它们并不会被回收直到下一次的老年代的回收。这种对象被称为漂浮垃圾。
        最后,由于没有压缩,碎片化或许会发生。为了处理碎片化,CMS会跟踪活跃的对象的大小,评估将来的需求,然后可能拆分或者合并空闲的空间来满足需求。
        并不像另一些回收器,CMS并不会在老年代变满时才开始老年代的回收。代替,它尝试更早开始回收工作,以便于它能够在老年代满之前完成回收。否则,CMS回收器将会返回到消耗更多时间的暂停mark-sweep-compact算法,像Parallel和Serial回收器所采用的。为了避免这种情况,CMS回收器将基于一些统计指标来启动,如之前回收的次数和老年代被占有的速度。如果老年代的占有率超过了初始设置的占有率,CMS回收器也会启动。初始化占有率设置,可以通过命令行参数-XX:CMSInitialingOccupancyFraction=n,n表示占有老年代的比例。默认是68。
        总的来说,相比Parallel回收器,CMS回收器有时会显著降低了老年代的暂停时间,代价是轻度的加长了年轻代的回收暂停时间,吞吐量的下降,更大的堆要求。
  3. 新增模式
        CMS可以运行在一种模式下,其并发阶段被递增地处理。这种模式意味着可以降低长时间并发阶段的影响,通过阶段性的暂停并发阶段,运行应用。回收任务被拆分为小的时间片,安排在两次年轻代回收之间。当应用需要在较少数量CPU的机器上运行并发回收器有较低的延迟时间,这种特性非常有用。更多信息,请查看“Tuning Garbage Collection with the 5.0 Java Virtual Machine”论文的第9章。

  4. 何时使用CMS
        如果应用需要更短的垃圾回收暂停时间,且更担负得起在应用运行时可以共享处理器资源,那么可以使用CMS回收器(因为并发的特性,CMS回收器在回收循环中会从应用抢占CPU的时间周期)。典型的情况,对于那些具有大量长时间存活数据(一个较大的老年代)以及运行在多CPU环境下的应用将会受益于CMS回收器。一个示例就是web服务器。CMS回收器可以被任何有低延时需求的应用考虑。对于在单处理上有适度大小的老年代的交互式应用,也会有一个好的表现。

  5. CMS的可选项
        如果你想使用CMS回收器,你必须声明命令行参数:-XX:+UseConcMarkSweepGC。如果你希望它运行在递增模式,可以通过-XX:+CMSIncrementalMode来使其生效。

第五章 工效学-自动化回收和行为转化

在J2SE 5.0的发行版中,垃圾回收器、堆大小、HotSpot虚拟机(client 还是server)的默认值是基于应用运行的操作系统和平台自动选择的。自动化的选择更好的匹配了不同类型应用的需求,而且比之前的版本要求更少的命令行参数。
    此外,一种新的切换回收器的方式也已经加入到并行垃圾回收器中。通过这种方法,用户说明希望的行为,垃圾回收器自动修改堆的尺寸,尝试获取期望的行为。根据平台依赖的默认选型和垃圾回收器切换的组合达到期望的行为被认为是工效学。工效学的目的就是提供更好的JVM执行能效,而需要更少的命令参数。
    自动化回收器、堆尺寸、虚拟机(运行模式)选择
    Server类型的机器通常定义为:

  • 超过2个CPU
  • 超过2G内存
        Server类型的定义可以应用到所有的平台上,除了32位的windows系统的机器。
        非server模式的机器,JVM、垃圾回收器、堆尺寸的默认值是:
  • client虚拟机
  • Serial回收器
  • 4MB的初始堆大小
  • 64MB的最大堆大小
        在server类型的机器上,JVM总是定义JVM为server模式,除非显示声明-client命令行参数要求client模式的JVM。在运行着server模式JVM的机器上,默认的垃圾回收器是Parallel回收器。否则,默认是Serial回收器。
        在server类型的机器上,以Parallel回收器的JVM(无论以client模式还是server模式运行),默认的初始化和最大堆尺寸是:
  • 初始堆尺寸为物理内存的1/64,最大是1G。(注意最小的初始化堆内存为32MB,因为Server模式的机器至少有2GB,其1/64是32MB)
  • 最大堆内存是物理内存的1/4,最大是1G。
        另外,非server模式的机器也同样使用默认值(4MB的初始化堆内存,最大64MB的堆内存)。默认值也可以通过命令行参数来修改,相关选择在第八章可以看到。
    基于行为的Parallel回收期切换
        在J2SE 5.0的发行版中,一种新的切换方法被加入到Parallel回收器中,基于垃圾回收器下应用的行为。命令行选项用于说明期望的行为,最大暂停时间和应用的吞吐量。
    1.最大暂停时间
        最大暂停时间是通过如下命令行选项声明的:
    -XX:MaxGCPauseMillis=n
        这对于回收器来说可以理解为一种暗示,希望n或者更少的暂停时间。Parallel回收器将会基于相关的参数调整堆的大小和另外的垃圾回收,尝试保持垃圾回收的暂停时间低于n毫秒。那些调整会导致垃圾回收器较低整体的应用吞吐量,某些情况下,预期的暂停时间是无法满足的。
    最大暂停时间的目标会单独应用到每个分代。如果目标无法满足,分代会变的更小以达到满足目标。默认情况没有最大暂停时间的设置。
    2.吞吐目标
        吞吐量的测试时根据垃圾回收的时间和非垃圾回收的时间(应用时间)。该目标是通过如下命令选项设置:
    -XX:GCTimeRatio=n
        垃圾回收时间与应用时间的比例是:
    1/(n+1)
        举例来说,-XX:GCTimeRatio=19表示有5%的时间用于垃圾回收。默认的值为1%。这个时间消耗表示的所有分代垃圾回收消耗时间的综合。如果吞吐量没有满足,会增加代尺寸,努力增加应用在两次垃圾回收之间运行的时间。较大的代需要更多时间来填满。
    3.覆盖目标
        如果吞吐量和最大暂停时间已经有满足,垃圾回收器将会降低堆的大小直到某个目标无法满足。不能满足的目标就会被处理。
    4.目标属性
        Parallel垃圾回收器会尝试首先满足最大的暂停时间。只有满足之后,才会考虑吞吐量的目标。相似地,覆盖率目标是在前两个目标满足之后考虑的。

第六章 建议

上一章节描述的工效学所产生的自动化垃圾回收,虚拟机和堆尺寸选择,对大部分应用都是合理的。因此,初始的对垃圾回收器选择和配置的建议是什么都不做,即并不需要声明任何垃圾回收器的使用方法。让系统根据应用运行的平台和操作系统自动选择。然后测试系统。如果他的运行效果具有足够高的吞吐量,足够低的延时,那么它是可以接受的,你的工作就完成了。你不需要寻找故障或者修改垃圾回收器的选项。
    另一方面,如果你的应用在垃圾回收上面有一些运行能效问题,那么你能做的最容易的一件事件是思考一下默认的垃圾回收器对于你的应用和平台的特性是否合适。如果不,选择一个合适的回收器,看一下是否运行行为变的可接受。
    你可以通过使用第七章描述的工具来测试和分析运行能效。根据其结果,考虑是否要修改选择,例如堆尺寸、垃圾回收的行为。一些常见的选项会列举在第八章。注意:执行切换的最好方案是先测试,然后再切换。进行一些与你的代码运行相关的测试。同时,也要考虑到过优化,因为应用数据集,硬件等-甚至是垃圾回收器的执行-都会随着时间而改变。
    本章节将会提供一些信息关于如何选择垃圾回收器和说明堆尺寸。然后,提供一些切换Parallel回收器的建议,并对如何处理OOM给一些忠告。
    何时选择一个不同的垃圾回收器?
    第四章讲述了每一种回收器建议的使用场景。第五章描述Serial回收器或者Parallel回收器是如何被平台默认自动化选择的。如果你的应用或者运行环境的特性就是一个不同的回收器比默认的回收器更是想要的,那么可以通过如下命名进行明确的要求:

  • -XX:+UseSerialGC
  • -XX:+UseParallelGC
  • -XX:+UseParallelOldGC
  • -XX:+UseConcMarkSweepGC
    堆尺寸
        第五章讲述了默认的初始化和最大堆内存是多少。那么尺寸对许多应用来说可能是运行良好的,但是如果你对性能问题的分析或者OOM表示整个堆上某个特定分代的大小存在问题,你可以通过第八章的命令选项来修改尺寸。举例来说,一个非server类型的机器的最大堆内存是64MB,你可以通过-Xmx来修改一个更大的堆内存。除非你有长时间暂停的问题,尝试保持尽可能多的堆内存。吞吐量和可用内存的量是成比例。足够的内存也是垃圾回收能效的最重要影响因素。
        在确定了你能够支付得起的内存的多少给予整个堆之后,可以考虑调整不同分代的大小。第二个影响垃圾回收能效的重要因素是年轻代占用堆的大小。除非你发现过度老年代回收或者暂停时间过长,否则尽量给予充足的堆内存给年轻代。但是,当你使用Serial回收器是,不要给予年轻代内存超过整个堆内存的1/2。
        当你使用Parallel回收器时,说明希望的行为比严格限制对尺寸要更好。让回收器自动自动和动态的修改堆尺寸已达到希望的行为,就像下一张所描述的。
    Parallel回收器的切换策略
        如果选择的垃圾回收器是Parallel回收器或者Parallel压缩回收器,进一步说明一个对你的应用是足够了的吞吐量目标。对于堆来说,不要选择一个最大值,除非你认为你需要一个超过默认最大堆的堆。堆将会扩展或者缩小已达到选择的吞吐量目标。在初始化和应用行为变更时,堆的尺寸需要相对应的变化。
        如果堆的大小达到最大值,大部分情况这意味着在这个最大尺寸下吞吐量目标是无法满足的。设置堆的尺寸到接近平台物理内存但又不引起应用的切换的大小。再次运行应用,如果吞吐量始终无法达到,意味应用的时间目标在当前平台的可用内存上被设置的过高了。
        如果吞吐量目标可以达到,但是暂停时间过长,选择一个较大的暂停时间目标。选择最大的暂停时间意味你的应用吞吐量目标无法达到,因此选择一个应用可以接受的折衷大小。
        堆的大小会随着垃圾回收器尝试满足竞争目标而变化,即使应用已经达到某种稳定状态。获取吞吐量目标的压力会同最大的暂停时间和最小覆盖率的目标相竞争。
    如何处理OOM(OutOfMemoryError)
        许多开发者不得不面对一个常见问题是OOM,应用会停止。这个错误在没有充足的内存分配一个对象时发生。这就意味着垃圾回收器不能可能回收到更多的内存来容纳一个新的对象,堆也不能进一步扩展。OOM问题并不一定意味着缺少堆内存。问题可能仅仅是一个配置的问题,比如声明的堆内存对应用来时是不够的。
        对于OOM的诊断,第一步是检查完整的错误信息。在错误信息中,进一步的信息会在“java.lang.OutOfMemoryError”之后提供。有些常见的示例关于额外信息是什么,意味着什么,该如何做:
  • Java堆空间
        这表示一个对象不能够在堆内存上分配内存。问题或许仅仅是一个配置问题。举例来说,你会得到这个错误,如果最大堆内存通过-Xmx命令行选项进行了说明,而其值对应用来说是不足够的。这也可能暗示一些不再被需要的类无法被垃圾回收器回收,因为应用无意间还保留这些类的引用。HAT工具可以用于搜索所有可达的对象,发现那些引用是始终被保留。这个错误的另一个潜在错误源是应用对终结器的滥用,比如引用终结器的线程不能跟上终结器添加到队列的速率。Jconsole工具可以用于监控那些类正在使终结器悬而未决。
  • 永久代空间
        这表示永久代已经满了。就像之前描述的,堆中有一块区域是专门用来存储元数据的。如果一个应用加载了大量的类,那么永久可能会超标。你可以通过命令行选项-XX:MaxPermSize来说明永久代的大小。
  • 要求一个数字大小超过VM的限制
        这表示应用尝试分配一个内存超过剩余堆内存的对数组。比如,如果一个应用尝试分配一个内存超过512M的数组,而最大的堆内存只有256M,那么错误就会抛出。在大部分情况下,这个问题可能是堆内存太小或者有bug导致应用尝试获取一个大小被错误计算的数组。
        在第七章描述的一些工具可以用于诊断OOM。最常用的工具是HAT,jconsole,或者带有-histo选项的jmap。

第七章 评估回收能效的工具

有多种诊断和监控工具可以用于评估垃圾回收的执行效果。本章节将对它们中的一些提供一个简要的概述。更多信息可以查看,第九章的“Tools and Troubleshooting”链接。
-XX:+PrintGCDetails命令行选项
    获取垃圾回收初始化信息的最简单方式之一是设置-XX:+PrintGCDetails命令行选项。每一次回收,都会输出每个分代在垃圾回收之前和之后,存活对象的多少,可用空间的多少,回收耗时。
-XX:+PrintGCTimeStamps命令行选项
    如果已经使用了-XX:+PrintGCDetail命令选项,那么会每一次输出的回收信息前加上时间信息。时间信息可以帮助你关联具有另外日志事件的回收日志。
jmap
    jmap是一个命令行工具包含在Solaris和Linux操作系统的(不包括window)的JDK发型版中。它会打印一些运行的JVM中与堆相关的统计信息或者核心文件。如果直接使用它不带用任何选项,它会打印加载的共享类列表,类似于Solaris的pmap工具集的输出。如果需要某些特殊信息,可以使用-heap、-histo、-permstat。
    heap选项用于获取包含垃圾回收器名称在内的信息,详细的算法(例如用于并行回收的线程数量),堆配置信息,堆使用率信息。
    histo选项用于获取获取堆中类的直方图统计。对于每个类,都会打印其在堆中的对象数量,哪些类消耗的内存大小,以及规格化的类名。直方图对于理解堆是如何使用的非常有用。
    配置永久代的大小对一些需要生产或者加载大量类的应用是非常重要的。如果一个应用加载的太多的类,OOM就会抛出。jmap的permstat选项可以用于获取永久代中类的统计信息。
jstat
    jstat工具集使用JVM的内嵌仪器提供一些运行应用的能效和资源消耗信息。诊断能效问题,特别是与堆尺寸和垃圾回收相关的问题,这个工具可能有用。它的一些选项可以打印关于垃圾回收和各代容量和消耗的统计信息。
HPROF: Heap Profiler
    HPROF是JDK 5.0中一个简单分析代理。它是一个动态链接库,通过JVM的工具接口联系JVM。它可以写出分析信息到文件或者通过以ASCII或者二进制的方式发送socket。这些信息可以紧接着通过一个分析工具进行处理。
    HPROF包括CPU使用率、堆分配统计、监控器竞争分析。另外,它可以输出完整的堆存储,报告所有JVM中监视器和线程的状态。HPROF对于分析能效,锁竞争,内存泄漏,及其他问题非常有用。
HAT: Heap Analysis Tool
    HAT可以帮助查找无意的类保留。这个属于是用于描述一个不再被需要,却依然保持存活,因为其引用仍然通过一些路径存在在一个存活的对象中。HAT提供一些遍历的工具浏览类使用HAPOF生产的堆快照中的拓扑结构。这个工具信息一些查询,包括“显示这个类所有的引用路径”。可以看第九章一个HAT说明文档的链接。

你可能感兴趣的:(HotSpot虚拟机内存管理)