美文网首页
内存管理白皮书译文

内存管理白皮书译文

作者: 丑人林宗己 | 来源:发表于2020-12-21 23:31 被阅读0次

    内存管理白皮书

    简介

    Java™2平台标准版(J2SE™)的一个优点是它执行自动内存管理,从而使开发人员免受显式内存管理的复杂性。

    本文概述了Java HotSpot虚拟机(JVM)中的内存管理Sun的J2SE 5.0版本。它描述可用于执行内存管理的垃圾收集器,以及给出一些关于选择和配置收集器以及为打开的内存区域设置大小的建议收集器操作。它还可以作为一种资源,列出一些最常用的选项影响GC回收器的行为,并提供大量更详细文档的链接。

    第2节是为那些不熟悉自动内存管理概念的读者准备的。它有一个简短的讨论冠与要求程序员显式地为数据分配空间相比,这种管理的好处是什么?第3节概述了一般的GC概念、设计选择和性能指标。它还将一种常用的内存组织引入到称为代的不同区域基于对象的预期寿命。在大范围程序应用使用过程中,在降低GC暂时时间以及GC总成本上已经证明了分代思想的有效性。

    本文的其余部分提供了特定于HotSpot JVM的信息。第四节描述了这四种GC收集器,包括J2SE 5.0-6中的新GC收集器,以及关于分代内存组织结构的文档。对于每个收集器,第四节总结了GC算法类型的使用以及适合选择GC回收器的特定场景。

    第5节描述了J2SE 5.0发行版中结合了

    • 基于应用程序的运行平台和操作系统,自动选择GC 回收器, 堆大小以及hotspot jvm 的状态(客户端/服务端)
    • 基于用户特度的期望行为进行动态垃圾收集调优

    这种技术被称为人体工程学。

    第6节提供了选择和配置GC回收器的建议。它还提供了一些关于如何处理**OutOfMemoryErrors **的建议。第7节简要地描述了一些工具用于评估垃圾收集性能,第8节列出了最常用的命令行与垃圾收集器选择和行为相关的选项。最后,第9节提供了更详细的链接本文涵盖的各种主题的文档。

    显示 VS 自动的内存管理机制

    内存管理是这样一个过程:识别何时不再需要已分配的对象,释放这些对象使用的内存,并使其可用于后续的分配。在一些编程语言中,内存管理是程序员的职责。该任务的复杂性导致许多常见错误,这些错误可能导致意外或错误的程序行为和崩溃。因此,大部分开发人员的时间都花在调试和纠正这些错误上。

    使用显式内存管理的程序中经常出现的一个问题是悬空引用。它是可以释放一个对象使用的空间,而其他对象仍然对该对象有引用。如果具有(悬空)引用的对象试图访问原始对象,但空间已重新分配给新对象,则结果是不可预测的,且不是预期的结果。

    显式内存管理的另一个常见问题是内存泄漏。这些泄漏发生在内存被分配、不再被引用但是没有被释放的时候。例如,如果你打算释放一个已使用的列表的内存空间,但你犯了错误仅仅释放列表的第一个元素,导致其余元素不再被引用但这部分内存空间已经跳出了程序的范围,既不能使用也不能恢复。如果发生了足够多的内存泄漏,它们会一直消耗内存,直到所有可用内存都耗尽为止。

    内存管理的另一种方法,目前已被广泛使用,尤其是在大多数现代应用中面向对象语言,是由一个叫做GC回收器的程序自动管理的。自动内存管理只需要增加抽象的接口和更可靠的代码。

    GC尽量避免了悬空引用问题,因为仍然存在一些被引用的对象将永远不会被垃圾收集,所以特不会被认为是已释放的。GC还解决了上面描述的空间泄漏问题,因为它自动释放所有不再引用的内存。

    GC概念

    GC的职责包括

    • 申请内存
    • 确保被引用的对象依旧存活在内存中(指不会释放还不能释放的已使用的内存空间)
    • 确保在可执行代码中不再可达的对象的内存可以被正确的释放回收

    被引用的对象被认为是活的,不再被引用的对象被认为是死的,称为garbage 。查找和释放(也称为回收)这些对象使用的内存的过程称为垃圾收集。

    垃圾收集解决了许多(但不是全部)内存分配问题。例如,可以无限地创建对象并继续引用它们,直到没有更多的内存可用为止。垃圾收集也是一项复杂的任务,需要花费大量的时间和资源。

    用于组织内存、分配和释放内存空间的精确算法由GC回收器处理,并对程序员透明。内存空间通常从称为堆的大内存池中分配。

    GC耗费的时间由GC回收器决定,通常情况下,当整个堆或者堆的某一部分消耗完,或达到一定阀值后会触发GC

    当涉及到在堆中查找一个未经使用,大小合适的内存块来完成一个内存分配请求的任务是较为困难的一个点,为了保证内存分配与释放回收过程的高效,很多动态内存分配算法都尝试规避内存碎片的发生。

    理想GC回收器特性

    GC收集器必须安全又全面,这意味着,存活对象占据的内存空间块不能被错误的释放,已死亡的对象占据的内存空间块不应该在超过少量回收周期的时间没有被正确处理。

    GC回收器期望可以做到高效运行,而不需要引入长时间的暂停(程序无法运行,称之stop the word)。然而, 与大多数的计算机相关的系统一样,需要在GC时间,堆空间和GC频率之间选择做适当的权衡。例如,如果堆很小,回收过程将会很快,但是堆也同样会很快被耗尽,因此需要更频繁的进行回收。相反,如果堆很大则需要更长的时间才能耗尽(或者达到既定阀值),因而回收的频率会降低,同时也意味着需要更长的GC时间。

    另一个理想的垃圾收集器特性是内存碎片的限制。当释放死亡对象的内存时,空闲空间可能以小块的形式出现在不同的区域中,这样在任何一个连续的区域中都可能没有足够的空间来分配大型对象。消除内存碎片的一种方法称为压缩,在各种垃圾收集器设计选择中进行了讨论。

    可伸缩性也很重要。分配不应该成为多处理器系统上多线程应用程序的可伸缩性瓶颈,回收也不应该成为这样的瓶颈。

    设计抉择

    当设计或者选择一款GC回收算法时需要考虑的一系列选择

    • 串行 还是 并行

      对于串行回收器而言,一次只发一件事,即便在多个CPU运行环境下,也只使用一个CPU来执行回收程序。对于并行回收器而言,GC回收任务被拆分成多个部分,这些子部分在不同的CPU上同时运行,由此可以达到更快的完成回收过程,但是会增加一些复杂性和潜在的内存碎片问题

    • 并发 还是 STW

      当STW的GC回收器运行时,在GC回收过程中,应用程序的运行线程也将会被完全挂起。不过,当一个或者多个GC任务被并发执行,与此同时,应用程序依旧可以正常运行。通常情况下,一个并发的GC回收器会并发的完成大部分的GC工作,但是也需要一些短暂的STW。STW GC 回收器 比并发GC回收器更加简单,因为STW GC回收器运行时堆是冻结的(不会进行内存分配与释放),对象状态也不会发生改变,它的劣势是一些程序并希望在运行过程中暂停(或者称较长时间的暂停)。相反地,在并行GC回收器中,暂停时间将会缩短,但是GC回收器必须格外小心,因为GC回收过程操作的对象的状态可能随着程序的运行会发生变化。这将会给并发GC回收器带来一些额外的开销,比如会影响到程序性能,需要更大的内存堆

    • 压缩 还是 非压缩 还是复制

      在GC回收器确认了对象在内存中是存活还是死亡后,它可以将内存进行压缩,将存活的对象移动至一起,从而将剩余的内存完全回收。在完成压缩后,在未分配(已回收)的内存空间上分配新对象即简单又快速。给对象分配内存空间时可以使用一个简单的指针来跟踪下一个可用内存空间地址。与压缩回收器相比,非压缩回收器回收对象内存并就地分配对象。比如,它不会像压缩回收器那样讲存活对象迁移至一起来创造一个大的可回收区域。好处是可以更快的完成内存回收,坏处是存在潜在的内存碎片问题。通常情况下,从具有就地回收的堆中进行分配要比从压缩后的堆中进行分配的成本更昂贵,因为需要在堆中搜索足以容纳新对象的连续内存区域。第三种选择是复制回收器,即复制存活的对象至另外的内存区域。好处是源空间可以被认为是空的,可用的,所以可以快速简单的进行后续的内存分配工作,但是潜在的问题是复制需要额外的时间,以及额外的内存空间。

    性能监控指标

    有几个指标被用来评估垃圾收集器的性能,包括:

    • 吞吐量( Throughput):在一段比较长的时间上去看,未花费在垃圾收集上的总时间百分比
    • GC开销(Garbage collection overhead):GC占总时间百分比
    • 暂停时间(Pause time):由于GC导致的STW所消耗的时间
    • GC频率(Frequency of collection):相对于程序的运行时间,GC发生的频率
    • 内存占用空间( Footprint—): 大小度量,比如堆代销
    • GC速度( Promptness): 对象死亡到对象占用内存释放成为可用内存的时间

    交互式应用程序可能需要较短的暂停时间,而总体执行时间对非交互式应用程序更重要。实时应用程序会要求在垃圾收集暂停和任何时间段花费在收集器上的时间比例上都有一个小的上限。占用空间小可能是在小型个人计算机或嵌入式系统中运行的应用程序的主要关注点。

    分代回收

    当使用一种成为分代回收的技术时,内存空间被划分为几代,即不同的内存空间存放着不同年龄阶段的对象,例如,最广泛使用的配置有两代,一种用于年轻对象,另一种用于老对象。

    在不同的代之间使用不同的算法去回收内存,每个算法都可以根据特定的代的常见观察特征进行优化。分代垃圾收集利用以下关于用几种编程语言(包括Java编程语言)编写的应用程序的观察结果(称为弱分代假设):

    • 大多数被分配的对象没有被长时间引用(被认为是活的),也就是说,它们死得早
    • 从较老对象到较年轻对象的引用很少

    年轻代回收器出现的相对频繁,而且效率高、速度快,因为年轻代空间通常很小,可能包含很多不再被引用的对象。

    对象经历过数次年轻代GC回收后将会被 提升(或晋升)到老年代。如下图,老年代通常情况下比年轻代会占用更多的内存空间,但是它们的使用率增长缓慢,所以老年代发生GC的频率更低,但是需要耗费更多的时间来完成GC。


    image.png

    为年轻代选择的GC回收算法通常非常重视GC速度,因为年轻代发生GC非常频繁。另一方面,老年代 通常由一种更节省空间的算法来管理,因为老年代占用了大部分的堆内存,而老年代算法必须在死亡对象占有率较低的情况下很好的工作。

    Garbage Collectors in the J2SE 5.0 HotSpot JVM

    J2SE 5.0 update 6 包括4中GC回收器,并且所有的GC 回收器都是分代回收器,该章节将会阐述分代思想,以及不同的回收器类型,并讨论为何内存分配经常快速且高效,它将会提供每个回收器的详细信息。

    HotSpot分代思想

    在Java HotSpot虚拟机中,内存由年轻代,老年代和永久代来组成。绝大多数对象初始分配位于年轻代,老年代包括一些从年轻代晋升(经历多次年轻代GC而存活)以及一些直接分配在老年代的大对象。永久代则存放一些JVM认为便于GC回收器管理的对象,如描述类和方法的对象,以及类和方法本身。

    年轻代由一个成为Eden的区域,加两个小的成为Survivor区域,比如下图。绝大多数对初始分配位于Eden区域。(一些较大的对象可能直接分配在老年代)Survivor持有那些至少在一次年轻代GC存活下来的对象,并且这些对象被认为在成为”足够老“的对象而晋升到老年代前,还有额外的死亡机会。(进入老年代也会被回收,这里的死亡机会是一种相对的说法)。绝大多数时候,一个Survivor 空间(标记为Form)持有这些对象,而另外一个是空的区域,知道下次回收才会被使用。


    image.png

    GC回收类型

    当年轻代内存空间被耗尽时,一次年轻代GC(有时候也称之为minor GC)会在年轻代区域运行。当老年代或者永久代区域内存空间耗尽时,一次full GC (有时候也称之为major GC)将会执行。至此,所有的分代都将被回收。通常情况下,优先回收年轻代,使用专门为年轻代设计的GC算法,因为它通常是识别年轻代中的死亡对象最有效算法。然而,在老年代和永久代上运行的是属于老年代回收算法的特定回收器,如果发生内存压缩,则两个代都将发生压缩。

    有时候,如果年轻代优先执行GC,老年代太满无法接受来自那些历经多次年轻代回收而存活下来将会晋升到老年代的对象。在这种情况下,除了CMS外,所有其他GC回收器都不会运行年轻代GC算法,取而代之的是在整个堆上使用老年代GC回收算法。(CMS老年代回收算法是一个特例,因为它无法回收年轻代)

    快速分配

    正如下你即将看到的关于GC回收器的描述,绝大多数情况下,都会有连续的大块可用内存空间来分配对象内存。使用简单的指针缓冲技术,内存分配是有效的。总而言之,跟踪上一个对象分配的结尾,当一个新的内存分配请求任务进来时,只需要判断剩余部分的内存空间是否满足新的对象的分配需求,如果是则更新指针并初始化对象。

    对于多线程应用程序而言,分配内存的过程需要确保线程安全。如果使用全局锁,那么分配过程将会成为瓶颈并降低性能。不过,HotSpot JVM使用了一种称为 Thread-Local Allocation Buffers(TLAB)的技术。它通过给每个线程独享的内存空间(分代区域内一小块空间)来提升多线程分配的吞吐量。所以在不需要任何锁的情况下,利用指针缓存技术可以在每个TLAB内快速的给单独的线程分配内存空间。但是,如果线程耗尽了一个TLAB后需要申请新的TLAB时,必须使用同步来确保安全。为了尽量减少使用TLAB而造成的内存空间浪费除了几个解决方案,例如,TLAB被分配器浪费的内存空间大小小于Eden平均1%的空间。TLABs的使用和使用bump-the-pointer技术的线性分配的结合使得每个分配都是有效的,只需要大约10个本机指令。

    串行回收器

    在串行回收器中,年轻代和老年代都只能串行工作(意味着只能使用单CPU资源),也会出现STW(意味着当回收过程进行时程序的运行将会被挂起)

    年轻代回收使用串行回收器

    如下图,它说明了年轻代使用串行回收器的一系列操作。存活在Eden的对象将会被拷贝至初始为空的Survivor 空间(标识To的区域),除了部分太大的对象无法进入Survivor To空间。类似这样的对象将会直接复制到老年代。部分相对年轻且存活的保存在Survivor Form空间的对象也会被复制到Survivor To空间,当个对象相对老之后救护复制到老年代。注意:如果Survivor To空间被耗尽,Eden空间和Survivor Form中存活的对象不会被复制而是被晋升,不过有多少次年轻代回收器,它们都将继续存活。在Eden 和Survivor From 中存活对象被复制完成后剩余的其他对象被定义为死亡对象,它们将不会被检查(在图中,这些将会被回收的对象被标记为X,虽然事实上回收器不会检查或者标记这些对象)

    image.png

    在一次年轻代回收完成后,Eden区域和以前占据Survivor空间(Survivor From)都会为空,而以前为空的Survivor空间(Survivor To)则会包含存活的对象,此时,两个Survivor空间将会将会互换角色,如下图:

    image.png

    老年代使用串行回收器

    使用串行回收器,通过标记-清除-压缩回收算法来回收老年代和永久代。在标记阶段,回收器将会标记哪些对象仍然是存活的。在扫描阶段,回收器会扫描整个分代,识别标记死亡对象并清除。然后,回收器将会执行滑动压缩,将存活的对象移动到老年代生产空间的开始部分,在另一端的单个连续快中留下任何空闲空间,如下图,压缩允许任何将来分配到老年代或永久代中使用快速的、插入指针的技术


    image.png

    何时使用串行回收器

    串行回收器是大多数运行在客户机类型的机器,且没有低暂停时间要求的应用程序。在今天的硬件上运行,创兴回收器可以有效管理许多重要的应用程序,它们的堆大小为64MB,对于完整的回收过程,最坏情况暂时时间也不会超过半秒。

    串行回收器选择

    在J2SE 5.0版本中,串行回收器被自动选择为非服务器级(client)机器上的默认GC回收器,如第5节所述。在其他机器上,可以使用-XX:+UseSerialGC命令行选项显式地使用串行回收器

    并行回收器

    今天,有许多的Java应用程序运行在有较多物理内存和多CPU的机器上,并行回收器,也称为吞吐量回收器,设计的目的在于提高多CPU的优势,而不是让多余的CPU限制仅通过单CPU来完成GC工作。

    年轻代使用并行回收器

    并行回收器使用的是基于串行回收器的年轻代回收算法的并行版本,它仍然会出现STW,仍然使用复制算法,但是它让回收过程在并行的环境上运行,使用多CPU,降低了GC回收的开销同时提高了应用程序的吞吐量。如下图说明了串行回收器与并行回收器之间的区别。

    image.png

    老年代使用并行回收器

    老年代GC使用的是跟串行回收器相同的标记-清除-压缩回收算法。

    何时选择并行回收器

    应用程序可以从并行回收器运行在多CPU的机器中受益,并且也没有停顿时间的限制,由于低频,所以老年代还是可能出现罕见的较长时间的暂停。比如哪些使用并行收集器通常包括那些做批处理,计费,工资单,科学计算等的应用程序。

    你可能想要考虑的是并行压缩GC回收器(下一节将要介绍)而不是并行回收器,因为前者可以并行处理所有的代,而不仅仅是年轻代。

    并行回收器选择

    在J2SE 5.0 RELEASE版本,并行回收器是自动被选择作为服务端类型的机器的默认GC回收器。在其他机器上,并行回收器可以通过显示的命令行来使用 -XX:+UseParallelGC

    并行压缩回收器

    并行压缩回收器是在J2SE 5.0 UPDATE 6版本引入,它与并行回收器的区别在于使用心得算法来回收老年代,注意:最终,并行压缩回收器将会取代并行回收器

    年轻代使用并行压缩回收器

    并行压缩回收器器的年轻代GC回收与并行回收器的年轻代GC回收使用相同的算法。

    老年代使用并行压缩回收器

    使用并行压缩回收器,老年代和永久代都会出现STW,大多数并行的方式是 滑动压缩。回收器主要包括三个周期,首先,每个代被逻辑划分为多个区域,在标记节点,从应用程序代码直接访问的存活对象的初始集被划分到GC回收线程中,然后并行地标记所有存活对象。当一个对象被识别为存活对象时,它所在区域的数据就会随着对象的大小和位置信息进行更新。

    统计阶段操作的是区域,而不是对象,由于上一次GC压缩,通常情况下每一个带的左边会更加密集,其中大多数对象都是存活的。回收这些区域所得到的效果不值得花费成本去压缩它们。所以,统计阶段第一件事是从最左侧开始检查密集的区域,直到某一个点,从一个可回收的区域的右侧区域的空间值得耗费成本进行压缩。这些区域的左侧是密集的存活对象,并且将不会移动区域的对象,该区域的右侧将被压缩,消除已经死亡的对象占据的内存空间。统计阶段计算并存储每个压缩区域的活动数据的第一个字节的新位置。注意,统计阶段目前是串行的,并行也是可能的,但对性能的影响不如标记和压缩阶段的并行化重要。

    在 压缩阶段,GC线程将会使用汇总数据来表示需要填充的区域,线程可以独立将数据复制到这些区域上,这样做的可以产出的堆一边高度密集,一边则可以空闲出一块较大的内存块。

    何时使用并行压缩回收器

    和并行回收器一样,并行压缩回收器有利于程序在多CPU环境上运行。除此之外,老年代回收操作降低了停顿时间,使并行压缩回收器更适合对停顿时间限制有要求的应用程序。并行压缩回收器可能不适合运行在大型共享机器上的应用程序,没有程序应该长时间占据多个CPU资源,在这样的机器上,可以考虑减少GC的线程数 (via the –XX:ParallelGCThreads=n command line option)或者选择另外的回收器。

    并行压缩回收器选择

    如果你想使用并行压缩回收器,可以选择使用命令行:-XX:+UseParallelOld

    并发标记清除(CMS)回收器

    对于许多应用程序而言,端到端的吞吐量不如快速响应时间来的重要,年轻代回收通常情况下不会导致长时间的停顿,然而,老年代回收器虽然频率不高,但会造成长时间的停顿,特别是大概涉及到大的堆时。为了解决这个问题,HotSpot JVM引入了一块称之为并发标记-清除(CMS)回收器,也称之为低延迟回收器。

    年轻代使用CMS回收器

    CMS回收器与并行回收器相同的方式回收年轻代。

    老年代使用CMD回收器

    绝大多数应用程序的老年代使用与应用程序运行并发处理的内存回收问题。

    一个CMS回收周期始于一个短暂的停顿,称之为初始话标记,它会根据程序代码直接标识存活对象的初始集合。然后,在并行标记阶段,回收器标识从这个集合传递可达的所有活动对象,因为在标记阶段,程序正在运行和更新应用字段,不是所有的存活对象都可以保证在整个标记阶段都被标记(为存活状态)。为了解决该问题,程序再次进行二次停顿,称之为重标记,它通过重新访问在并发标记阶段修改的任何对象来标记。由于重标记停顿比初始标记更重要,而且并行运行多个线程可以提高效率。

    在重标记结束时,所有的存活对象都可以确保被标记,所以随后的并发清除阶段可以回收所有被标记的死亡对象内存空间。如下图,说了串行并发标记-清除-压缩回收器和CMS之间的区别。

    image.png

    由于某些任务,比如在备注阶段重新访问对象,增加了回收器的工作了,所以它的开销也会增加。对于大多数尝试减少停顿时间的回收器来说是一种折中的办法

    CMS回收器是唯一的非压缩回收器。也就是说,在释放死对象占用的空间之后,它不会将活对象移动到老对象的一端。如下图:

    image.png

    它节省了一些时间,但是由于空闲空间不是连续的,所以回收器不能再时间简单的指针指向下一额空间位置来分配下一个对象的内存空间。相反,它需要使用一个空闲列表,它创建了一些链表指向了未分配的内存区域,每次对象需要分配内存空间时,列表需要寻找到一个足以容纳对象的区域来完成分配。结果,给老年代分配内存比使用简单指针技术更加昂贵。并且它也会增加年轻代回收的开销,毕竟很多占据老年代的对象是从年轻代晋升而来。

    CMS较之其他的回收器的另外一个劣势是需要较大的堆内存空间。考虑到程序在标记阶段还可以运行,它会续集申请内存空间,从而有可能继续增长(消耗)老年代的内存空间。除此之外,由于回收器确保在标记阶段期间会标识所有存活的对象,部分对象直到下一次老年代回收开始前也可能在这个周期间成为死亡对象,并且不会被回收。类似这样的对象称之为浮动垃圾

    最后,由于缺乏压缩功能,内存碎片问题还是会出现。为了解决内存碎片的问题,CMS追踪热点对象大小,预期未来需求,可能拆分或者加入一些空闲块来满足需求。

    不像其他的回收器,CMS不会在老年代内存空间耗尽时开始老年代回收。相反,它会在内存耗尽前足够早的时间开始回收确保耗尽前可以完成回收。另外,CMS恢复在串行回收和并行回收所使用的关于耗时的STW的标记-清除-压缩算法。为了避免该问题,CMS会根据之前静态的GC时间和老年代被耗尽的速度及时启动。CMS也会根据如果当前耗费的老年代内存空间的使用率超过了初始使用率来启动,该值通过命令行 –XX:CMSInitiatingOccupancyFraction=n,来设定,n是有默认占据老年代大小的百分比,默认值是68

    总而言之,对比并行回收器,CMS戏剧性地降低了老年代停顿时间——以稍微长一点的年轻代停顿、吞吐量降低和额外的堆大小需求为代价。

    增量模式

    CMS可以运行在一个并行周期增长的模式中,该模式意思是通过定期停止并发阶段来降低长并发阶段的影响,从而使应用程序能够进行回退处理。收集器所做的工作被划分为小块时间,这些时间被安排在年轻代收集之间。这个功能非常有用,当运行在较低的处理器(1 ,2 个)的机器上的应用程序有较低的停顿时间要求时。想要了解该模式的更多信息,可以查阅”Tuning Garbage Collection with the 5.0 Java™ Virtual Machine“文件的第9章。

    何时选择CMS

    如果你的应用程序需要更短的回收停顿时间,并且负担得起当程序运行时可以与GC回收器共享资源时使用CMS回收器。(由于它的并发性,CMS会在GC运行周期内从应用程序手中抢夺CPU时间片)。通常情况下,运行在有两个或更多处理器的机器上的应用程序具有相对大的长期存活的数据(一个大的老年代)是可以从该回收器中受益的。比如是一个WEB 服务器。CMS应该面向有低停顿时间需要的应用程序。在单处理器上运行具有中等大小的老年代的互动性应用程序可能也会有一个比较好的效果。

    选择CMS

    如果你想使用CMS回收器,你必须显示的选择特定的命令行:-XX:+UseConcMarkSweepGC.,如果你想使用增长性模式,你需要开启 –XX:+CMSIncrementalMode 选项

    人体工程学 - 自动选择和行为优化

    在J2SE 5.0版本,GC回收器的类型,堆大小,和HotSpot 虚拟机(客户端 or 服务端)已经基于应用程序运行的平台和操作系统做了默认值。这些默认值可以更好匹配不同类型的应用程序的需求,比起以前的版本仅仅需要几个命令行。

    除此之外,动态优化回收器的新方案也会被加入到并行回收器中。这个方案,用户指定需要的行为,GC回收器会动态的调整对区域的大小尽可能达到行为需求。平台相关的默认选择和使用所需行为的GC回收器调优这个组合将成为人体工程学,人体工程需的目标通过最低的命令行调优是提供好的JVM性能指标

    自动选择的回收器,堆大小,和虚拟机状态

    一个服务端机器定义具备如下一个:

    • 2 或更多的物理处理器
    • 2 或 更多G的物理内存

    该定义适用于所有的平台的服务端机器,运行在Windows操作系统上的32位平台可能存在差异。

    非服务端机器的,JVM关于GC回收器,堆大小的默认参数如下

    • client JVM
    • 串行回收器
    • 初始堆4MB
    • 最大堆64MB

    对于服务端机器而言,JVM一直运行在服务端状态上除非你显示通过-client命令行指定要求使用客户端JVM。运行在服务端的JVM,默认使用并行回收器,其他默认值跟串行回收器一致。

    服务端机器使用并行回收器运行JVM不管是客户端还是服务端状态,堆的初始默认值和最大默认值如下:

    • 初始值为物理内存的1/64,最大值为1GB(注意:最小初始值为32MB,因为服务端蒂尼为最小有@GB的内存,而2GB内存的1/64是 32MB)

    • 最大内存是物理内存的1/4,上线是1GB

    其他的,非服务端机器的相同默认值为4MB初始堆和64MB最大堆。默认值通常可以通过命令去修改,相关命令器看第8章。

    基于行为的并行收集器调优

    在J2SE 5.0 RELEASE版本,并行回收器增加了调优的新方法,敬畏GC回收器的前提下基于应用程序需要的行为。命令行用于指定需要的行为来达到最大停顿时间和应用程序吞吐量的指标。

    最大停顿时间目标

    最大停顿时间指定的命令行

    ​ -XX:MaxGCPauseMillis=n

    它指示并行回收器的停顿时间不得超过 n milliseconds。并行回收器将会调整堆大小和其他GC相关参数来尽可能保持、GC停顿时间不得超过 n milliseconds。这些调整可能导致GC降低程序的吞吐量,在有些情况所需的停顿时间指标深知不可达。

    最大停顿时间适合所有不同的分代。如果目标不可达,分代将变得更小来试图达到这个目标,没有最大停顿时间也会被默认设置。

    吞吐量目标

    吞吐量指标是通过测量耗费在GC的时间和耗在GC有以外的时间(被称为程序时间)该指标需要他通过命令行来指定:

    ​ -XX:GCTimeRatio=n

    GC时间与程序时间的比率为

    ​ 1 / (1 + n)

    比如,-XX:GCTimeRatio=19 是指目标5%的时间消耗在GC上,默认指标是1%(n = 99),耗在GC的时间是所有分代GC的时间总和。如果吞吐量指标不可达,分代区域将会提高来支撑应用程序在GC时的时间。更大的分代需要更多的时间来填补。

    内存占用量指标

    如果吞吐量和最大停顿时间指标都达标了,GC将会适当的降低对的大小知道有一个之标的不可达(降低吞吐量优先),指标不可达后会继续调整。

    指标优先级

    并行回收器尽量优先达到最大停顿时间指标,只有在它们达标后才会处理 吞吐量指标。类似的,内存占用量指标只会在两个指标之后达成。

    建议

    上一节指出的人体工程学导致自动选择GC回收器,虚拟机状态和堆大小对于绝对数应用程序是合理的。因此,初次建议是对于选择和配置GC最好是什么都不做。意味着不需要特定指定GC回收器,等等。让系统根据应用程序运行的平台和操作系统来做自动选择。然后测试你的应用程序,如果性能是可接受的——足够高的吞吐量和祖国地的停顿时间,你可以这么做,不需要进行故障排查和修改GC选项。

    另一方面,如果你的应用程序看起来存在跟GC相关的性能问题,你要做的第一件事根据你的应用程序和平台特性来思考GC默认参数是否合理,如果不合理,显示选择你认为合理的GC回收器,并且看看性能是否可以接受。

    你可以像第七章说的那样使用工具来测量和分析性能。基于这些记过,你可以考虑修改选项,比如类似控制堆大小 或者 GC回收器行为。一些最常见的参数会在第八章罗列出来。请注意:最好的调试性能方法是先测量,然后调优。测量使用你的应用程序代码先关的测试。其次,当心过分优化,因为你的程序的数据集合,软件等等——甚至GC实现!——可能随时改变。

    这个章节提供了一些关于选择一个GC回收器和指定堆大小的信息,根据这些建议来调优并行GC回收器。并且给出一些关于如何处理OutOfMemoryErrors的建议。

    何时选择GC回收器

    章节四提过,对于每一个GC回收器,都适合使用回收器的推荐的常见。章节五描述了平台默认选择的串行回收器还是并行回收器,如果你的应用程序或者运行环境的特性要去使用与默认回收器不同的回收器,根据如下的命令行显示指定所需的回收器:

    –XX:+UseSerialGC
    –XX:+UseParallelGC
    –XX:+UseParallelOldGC
    –XX:+UseConcMarkSweepGC

    堆大小

    章节五提到默认的初始化和最大堆大小,这些值也许适合应用程序,但是根据一次性能问题或者一次OutOfMemoryError(在随后的章节讨论)的分析表明特定的分代或者整个堆的大小存在问题或者,你可以根据这些命令修改这些值(参照章节八),比如,在非服务端机器上最大堆大小64MB通常大小,所以你可以通过–Xmx 指定一个大的值。除非你的问题是长 停顿时间,尝试授权更多的内存空间给堆。吞吐量与内存可用成大小比例,提供足够的可用内存空间比是影响GC性能最重要的因素。

    在你决定将所有的内存都给了堆之后,可以考虑调整内存至不同的分代区域。第二个最有影响力的因素是支持GC性能的是整个堆与年轻代专用堆的比例。除非你可以找到过大的老年代或者停顿时间存在的问题,否则给年轻代足够的内存空间。然而,当你使用串行回收器,你不需要给超过整个堆一半的内存空间给年轻代。

    当你使用并行回收器中的一个,更可取的是指定所需的行为而不是精确堆的大小。让回收器自动、动态修改堆的大小来达到目标行为,就像接下来描述的。

    并行回收器的调优策略

    在GC回收器的选择中(自动还是显示),是并行回收器还是并行压缩回收器,还是为自己的应用程序选择合适的特定吞吐量指标。不要选择一个堆的最大值除非你知道你的堆大小更优于默认的最大堆大小。堆的增长还是缩小都影响吞吐量指标。在初始化期间和应用程序行为更改期间,堆大小可能会出现一些波动。

    当堆增加到最大值时,在大多数情况一下意味着最大的内存空间无法满足吞吐量指标。将最大值设置为接近平台的最大物理内存量,并且不会导致发生应用程序的(虚拟内存)交换,再次执行程序,如果吞吐量指标依旧无法达标,这个指标对于平台上的可用内存而言,那么应用程序时间的目标对于平台上的可用内存来说太高了。

    如果吞吐量达标了,但是停顿时间太长了,选择一个最大的停顿时间指标。选择一个最大的停顿时间指标可能以为着你的吞吐量无法达标,所以需要作出一个对程序可以接受的妥协。

    当GC回收器试图满足竞争目标时,即使应用程序达到一个稳定状态,堆的大小也可能会发生波动。达到吞吐量指标竞争的压力(可能需要更大的堆)与最大停顿时间和最大占用最小空间的目标(两者都可能需要更小的堆)竞争。

    可以对OutOfMemoryError做什么

    一个非常普遍的问题是许多开发者需要解决应用程序因为java.lang.OutOfMemoryError而终止。这个错误是在当没有合适的空间分配内存给对象而抛出的。GC回收器不能使任何其他空间可用来容纳新对象,并且堆不能进一步扩展。OutOfMemoryError并不是暗示一定是内存泄漏。这个问题可能只是简单到一个配置的问题,比如,指定堆的小(或者未指定的默认大小)对应用程序来说不够。

    诊断OutOfMemoryError的第一步是检查内存满了的错误信息,在异常信息,更进一步的信息在“java.lang.OutOfMemoryError”之后提供。以下是一些常见的例子,这些额外的信息可能是什么,它可能意味着什么,以及如何应对:

    • Java heap space

      它指出对象无法在堆中分配到内存。这个问题也是只是一个配置问题。你可以取到这个错误信息,比如,如果由 –Xmx命令行所指定(或选择默认)的最大的堆内存值是无法满足应用程序的。这个问题也可能表明那些不再被需要的对象无法被GC回收,因为应用程序无意中持有它们的引用。HAT工具(见第七章)可以用来查阅所有可达对象并且了解哪些引用被持有以至于每个对象都存活着。这个错位的另外一个潜在问题可能是大量使用了finalizers,以致调用终结器的线程无法跟上队列中添加终结器的速度。jconsole管理工具可以 用来监控那些等待被清理的对象。

    • PermGen space

      它指出永久代内存空间不足,在很早之前就说过,永久代存放的是元数据。如果程序加载了大量的类,那么永久代就需要增加内存空间了。你可以通过使用命令行来指定:–XX:MaxPermSize=n

    • Requested array size exceeds VM limit

      它指出应用程序尝试分配一个超过堆大小的空间给数组。比如,应用程序尝试分配给数组 512MB,但是堆最大值只有256MB,那么该错误就会被抛出。大多数情况下,该问题看起来像是堆太小了,或者程序试图创建一个大小被错误得计算为过大的数组。

    第七章介绍了一些工具来诊断OutOfMemoryError问题。几个最常用的工具是 HAT,jconsole, jmap(与-histo一起出现)

    评估GC回收器性能指标的工具

    不同的诊断和监控工具被用来评估GC的性能,这个章节将会简述其中一些工具,更新信息请查阅第九章Tools and Troubleshooting的链接

    –XX:+PrintGCDetails Command Line Option

    指定命令行–XX:+PrintGCDetails是最简单的获取GC初始信息的方式之一。对于每个回收器,输出的信息比如不同代执行GC前后存活对象的大小,每个分代的最大可用内存空间,GC所耗费的时间。

    –XX:+PrintGCTimeStamps Command Line Option

    除了输出–XX:+PrintGCDetails 所输出的信息之外,会输出每次回收的开始时间戳,该时间戳可以帮助我们将GC日志与其他日志的时间关联起来。

    jmap

    jmap是一个在 Solaris™操作系统和Linux(不是Windows)发行的JDK版本进入的命令行。它可以在JVM运行时或者核心文件 打印内存相关的统计信息。如果不使用任何命令行选项,那么它将打印加载的共享对象列表,与Solaris pmap实用程序输出的内容类似 。对于更具体的信息,可以使用-heap、-histo或-permstat选项。

    -heap选项用于获取包括GC回收器名称,算法细节(比如用于并行回收器的线程数),堆配置信息,以及堆使用情况统计

    -histo选项有用于获取堆的类级直方图。对于每个类,它打印堆中的实例,这些对象消耗内存的总字节数,和全限定类名。当试图理解堆的使用情况,直方图非常有用。

    对于动态生成或者加载非常多的类信息的应用程序(比如Java Server Pages™或者WEB容器)而言,配置永久代的固定大小非常重要。如果程序加载了大量的类,可能会抛出OutOfMemoryError,jmap命令的–permstat 选项可以获取永久代中的对象统计信息

    jstat

    jstat工具是HotSpot JVM内存工具中用来提供有关程序运行性能和资源消耗的实用工具。该工具用于诊断性能问题,特别是与堆大小和GC相关的问题。该工具的一些选项用于打印关于GC行为和不同代的容量的统计信息。

    HPROF: Heap Profiler

    HPROF是JDK 5.0附带的一个简单的分析器代理。它是一个动态链接的库,使用Java虚拟机工具接口(JVM TI)连接到JVM。它以ASCII或二进制格式将分析信息写入文件或套接字。这些信息可以通过分析器前端工具进一步处理。

    HPROF能够显示CPU使用情况、堆分配统计信息和监视器争用分析。此外,它可以输出完整的堆dump 并报告Java虚拟机中所有监视器和线程的状态。HPROF在分析性能、锁争用、内存泄漏和其他问题时非常有用。有关HPROF文档的链接,请参见第9节。

    HAT: Heap Analysis Tool

    堆分析工具(HAT)帮助调试无意的对象保留。此术语用于描述不再需要的对象,但由于通过来自活动对象的某些路径的引用而保持活动的对象。HAT提供了在使用HPROF生成的堆快照中浏览对象拓扑的方便方法。该工具允许许多查询,包括“显示从rootset到该对象的所有引用路径”。有关HAT文档的链接,请参见第九章节

    GC 相关的关键options

    很多命令行可以用于选择有一个GC回收器,指定堆或者分代的大小,修改GC回收器行为,获取GC回收器统计信息。这个章节将会展示一些最常用的命令行,有关各种可用选项的更完整列表和详细信息,请参见第9章,注意:您指定的数字可以以“m”或“M”(MB)结尾,以“k”或“K”(KB)结尾,以“g”或“G”(GB)结尾

    image.png
    image.png
    image.png
    image.png
    image.png

    PS:好久以前的文章,也不知道结束了没有……

    相关文章

      网友评论

          本文标题:内存管理白皮书译文

          本文链接:https://www.haomeiwen.com/subject/omlwpctx.html