【笔记】memorymanagement-whitepaper-150215

时间:2022-02-11 09:16:56

3 GC概念

Gc的职责:

1)  分配内存

2)  保证被引用的对象驻留内存

3)  对象不可达后将其占用内存回收

被引用对象被称为 “存活对象”。

不再被引用的对象称为“垃圾对象”。

找到垃圾对象并将其占用内存释放的过程被称为垃圾收集。

垃圾收集耗费的时间取决于选择的垃圾收集器。

大多数动态内存分配算法需要面临的问题是在搞笑分配和回收内存时如何避免碎片。

期望的垃圾收集器特征

垃圾收集器必须保证安全和全面。

不能错误地将存活对象释放,垃圾对象也要保证在几个回收轮次内回收掉。

垃圾回收要非常搞笑,不能停顿过长时间而导致应用无法运行。

一般需要在时间、空间、频率几个方面做一个权衡。

垃圾回收要限制碎片产生。通过压缩来消除碎片。

可伸缩性也非常重要。对象分配和回收不能成为多CPU系统上运行的多线程应用可伸缩性的瓶颈。

Design Choices

设计决策

1、  Serial 对 Paralle

Serial收集时,不能执行其他应用。只利用一个CPU。

并行收集,将收集任务分为多个子任务,它们在不同CPU上同时执行,速度更快,但有额外的复杂性,可能产生碎片。

2、  同步 对 暂停

Stop-the-world 垃圾收集启动期间,应用的执行会完全暂停。

并发垃圾收集器的一个或多个任务会和应用同时运行,其大部分工作都是并发执行的,但偶尔也需要短时间暂停应用执行,其需要更大的堆内存。

3、  压缩 对 不压缩 对 拷贝

识别存活对象并将其移动到一起,然后将其他不用空间释放。--压缩。

压缩后可以从第一个可用地址空间来分配对象,速度更快,更简单,用一个指针跟踪下一个

可用地址。

非压缩收集器仅仅把垃圾对象原地释放,其手机速度更快,但缺陷是产生垃圾碎片,而且分配对象更复杂。

拷贝收集器,将存活对象拷贝到一个不同的内存区,更快,更容易分配,但拷贝需要耗时,也需要额外空间。

Performance Metrics

性能度量。

评估性能。

1)  吞吐量。长时间内非gc时间的百分比

2)  Gc开销。长时间内gc时间的百分比

3)  停顿时间。Gc过程中应用暂停的时间

4)  收集频率。

5)  空间大小。占用的堆大小

6)  Promptness。即时性。对象变为垃圾数据到其内存被回收的时间。

交互性应用要求较低的停顿时间,非交互式则要求吞吐量,实时应用对gc的停顿时间和吞吐量都要求较高。

分代收集。

内存划分为几个“代”,不同的代保留不同存活时间的对象。

每个分代有适合的算法。

Weak generation hypothesis。

大多数对象存活很短

从老年代引用新生代对象的情况很少

新生代中的对象经过几次收集依然存活的会被提升到老年代。

老年代一般比新生代占比更大,增长更缓慢,收集频率低,但耗时长。

新生代垃圾收集算法着重速度,其收集频繁,老年代收集要求空间效率高。

Garbage Collectors in the J2SE 5.0 HotSpot JVM

回收算法。

Hotspot的分代:新生代,老年代,永久代。

绝大多数对象在新生代中分配。

老年代包含从新生代中提升的或者初始就非常大的对象。

永久代,包括JVM使用的元数据。

新生代由三部分构成: Eden、两个survior区。

大部分对象在eden中初始化分配。

Survivor区保留至少存活过一次收集的对象。有一个survior一直为空。

垃圾收集类型

新生代填满后,新生代开始收集(minor collection)。

老年代满了之后做major collection,此时所有的粉黛都会收集。一般会先收集新生代,

然后使用 老年代收集算法 收集老年代和永久代,如果需要压缩,每个分代都独立压缩。

但是。。。当major gc时,如果先对新生代收集,并且老年代没有足够空间容纳新生代提升上来的对象时,就不会再使用新生代收集算法,而是对所有的堆都使用 老年代收集算法。例外就是CMS收集器,因为它不能对新生代进行收集。

快速分配

分配对象的操作需要保证线程安全。

此时分配操作容易成为瓶颈,降低性能。

JVM 采用TLABs(Thread-Local Allocation Buffers)的方式。线程本地分配缓存。

它给每个线程提供自有的缓存—堆的一小部分,线程从中进行分配对象。

这样每个线程分配对象时速度都很快,一旦线程将其TLABs填满后,就需要给它新分配一个,这时就需要同步了。

小于 1%的浪费。不超过10个本地指令。

Serial collector

使用serial 收集器,新生代和老年代收集都是串行的,使用一个CPU,会导致应用暂停。

新生代使用Serial 收集器

1)  将eden中的存活对象进行识别拷贝到 空的 survior区,过大的直接提升

2)  另一个survior中依然存活的对象,够条件的提升,不够的移到另一个survior,如果survior满了,直接提升。

3)  Eden直接释放,eden为空了

收集后的新生代:

对老年代的串行收集

老年代和永久代的收集通过:mark-sweep-compact收集算法。---串行

标记阶段:收集器识别存活对象

清理阶段:将垃圾对象清理掉

压缩阶段:将存活对象拷贝到一端,剩余的一端变成连续的可用空间。

使用条件

Client模式,对暂停时间要求不高。

-XX:+UseSerialGC

并行收集器

吞吐量收集器。可以利用多个CPU。

新生代使用并行收集器

仍然需要暂停应用执行,但是将收集任务分为多个子任务,每个都使用一个CPU执行,降低了执行开销,提升吞吐量。

老年代使用:

Mark-sweep-compact算法。也利用多个CPU。

使用条件

多个CPU,无停顿时间限制

-XX:+UseParallelGC

并行压缩收集器

新的老年代收集算法。

新生代使用:

依然是并行收集。

老年代使用:

三个阶段:

1)  每个分代逻辑划分为多个区

2)  标记阶段:多个线程同时标记出存活对象

3)  存活对象所在区域信息更新,标记出该区域存活对象的位置和大小。

4)  Summary阶段,针对区域操作,左侧区域占用率较高,对这种区域进行压缩,不值得。从左侧开始检查每个区域的空间使用率,直到找到一个区域,其右侧的都值得压缩。此时将待压缩区域的对象情况进行记录。

5)  压缩阶段:垃圾收集器使用多个线程,将summary阶段识别的数据拷贝填充。完毕后右侧会有一个连续较大的内存空间。

使用条件:

使用多个CPU。减少了停顿时间。

-XX:+UseParallelOldGC    -XX:ParallelGCThreads=n

并发标记清理收集(CMS)

响应时间优先。

老年代收集可能带来较长时间的停顿,特别是堆较大时。低延迟收集器。

新生代:使用并行收集

老年代:

初始化标记:短暂的停顿,识别当前直接可达的存活对象

同步标记阶段:标记从初始标记的对象简介可达的所有存活对象,由于系统并发执行,此阶段并不能标记所有的对象

重标记阶段:再次停顿,重新便利并发标记阶段被修改的对象,该阶段停顿时间比初始化标记阶段更长,采用多线程来提高效率。

同步清理阶段:将识别的垃圾对象清理掉。

该算法不会进行压缩。

它使用一个列表保存可用空间地址。造成分配操作复杂耗时。影响新生代的效率。

需要更大的堆空间,因为收集和应用是并发执行的,收集期间会继续分配使用。

收集期间可能一些对象已经变成垃圾数据,这部分清理不掉,只能等到下次收集,被称为floating garbage。

CMS收集器跟踪分配的对象大小,估计后续的需要,有可能将可用空间划分或者集合起来。

CMS不会的等待满了才收集,会稍微提前。一旦老年代满了,CMS会转而是用标记清理压缩算法,为避免这种情况,CMS根据之前收集的次数和耗时的统计数据来自行启动。

CMS也会根据配置的占用率来启动。 -XX:CMSInitiatingOccupancyFraction=n,n代表老年代尺寸的百分比,默认68.

CMS减少老年代的停顿时间,延长了新生代的停顿时间,稍微减少吞吐量,额外的堆。

增量模式

并发阶段增量式进行。

减少长时间并发的影响,转而阶段性停止并发阶段,释放CPU用于执行应用程序。

收集器的工作划分为小的时间段,在新生代收集之间进行。

对于CPU较少但又希望低延迟的应用很有用。

条件

需要更短的停顿,多CPU。

对于长期保留大量存活对象,多余2个CPU的应用有效,例如web server。

-XX:+UseConcMarkSweepGC   -XX:+CMSIncrementalMode

GC

新生代

老年代&永久代

特点

串行

1)  将eden中的存活对象进行识别拷贝到 空的 survior区,过大的直接提升

2)  另一个survior中依然存活的对象,够条件的提升,不够的移到另一个survior,如果survior满了,直接提升。

3)  Eden直接释放,eden为空了

老年代和永久代的收集通过:mark-sweep-compact收集算法。---串行

标记阶段:收集器识别存活对象

清理阶段:将垃圾对象清理掉

压缩阶段:将存活对象拷贝到一端,剩余的一端变成连续的可用空间。

Client模式,对暂停时间要求不高。

-XX:+UseSerialGC

并行

仍然需要暂停应用执行,但是将收集任务分为多个子任务,每个都使用一个CPU执行,降低了执行开销,提升吞吐量。

Mark-sweep-compact算法。

多个CPU,无停顿时间限制

-XX:+UseParallelGC

并行压缩

依然是并行收集。

三个阶段:

1)  每个分代逻辑划分为多个区

2)  标记阶段:多个线程同时标记出存活对象

3)  存活对象所在区域信息更新,标记出该区域存活对象的位置和大小。

4)  Summary阶段,针对区域操作,左侧区域占用率较高,对这种区域进行压缩,不值得。从左侧开始检查每个区域的空间使用率,直到找到一个区域,其右侧的都值得压缩。此时将待压缩区域的对象情况进行记录。

5)  压缩阶段:垃圾收集器使用多个线程,将summary阶段识别的数据拷贝填充。完毕后右侧会有一个连续较大的内存空间。

使用多个CPU。减少了停顿时间。

-XX:+UseParallelOldGC    -XX:ParallelGCThreads=n

CMS

使用并行收集

CMS不能对新生代收集

初始化标记:短暂的停顿,识别当前直接可达的存活对象

同步标记阶段:标记从初始标记的对象简介可达的所有存活对象,由于系统并发执行,此阶段并不能标记所有的对象

重标记阶段:再次停顿,重新便利并发标记阶段被修改的对象,该阶段停顿时间比初始化标记阶段更长,采用多线程来提高效率。

同步清理阶段:将识别的垃圾对象清理掉。

该算法不会进行压缩。

它使用一个列表保存可用空间地址。造成分配操作复杂耗时。影响新生代的效率。

需要更大的堆空间,因为收集和应用是并发执行的,收集期间会继续分配使用。

收集期间可能一些对象已经变成垃圾数据,这部分清理不掉,只能等到下次收集,被称为floating garbage。

CMS收集器跟踪分配的对象大小,估计后续的需要,有可能将可用空间划分或者集合起来。

CMS不会的等待满了才收集,会稍微提前。一旦老年代满了,CMS会转而是用标记清理压缩算法,为避免这种情况,CMS根据之前收集的次数和耗时的统计数据来自行启动。

CMS也会根据配置的占用率来启动。 -XX:CMSInitiatingOccupancyFraction=n,n代表老年代尺寸的百分比,默认68.

CMS减少老年代的停顿时间,延长了新生代的停顿时间,稍微减少吞吐量,额外的堆

CMS增量模式

并发阶段增量式进行。

减少长时间并发的影响,转而阶段性停止并发阶段,释放CPU用于执行应用程序。

收集器的工作划分为小的时间段,在新生代收集之间进行。

对于CPU较少但又希望低延迟的应用很有用。

自动选择及行为优化

5.0后,JVM会根据应用所在的系统自动选择收集器、堆、jvm模式。

自动选择垃圾收集器、堆大小、JVM模式

Server-class机器环境:

至少2个物理处理器

至少2G物理内存

除了32位的windows操作系统,其他平台都使用上述定义。

对于非server-class机器,默认参数:

1)      Client JVM

2)      串行垃圾收集

3)      初始堆4M

4)      最大堆64M

Server-class机器上,server模式时默认为并行收集器。

Server-class机器上JVM使用并行收集器时默认参数为:

1)  初始堆,物理内存 1/64,最大1G。

2)  最大堆: 1/4物理内存,最大1G。

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

最大停顿时间目标: -XX:MaxGCPauseMillis=n

该参数提示并行收集器停顿时间不要超过 n 毫秒。收集器会调整堆大小和其他相关参数去尽力达到停顿时间目标。但这些调整由可能降低系统吞吐量。

吞吐量目标

吞吐量:gc时间和非gc时间。

-XX:GCTimeRatio=n

Gc时间为: 1/(1+n)

占用量目标

吞吐量和停顿时间都达到后,gc会减少堆直到其中一个目标不满足(一直都会是 吞吐量目标),不满足目标后,gc再调整来满足该目标。

-========那岂不是一直在抖动?????

目标的优先级

并行收集器,会首先去满足最大停顿时间目标。然后是吞吐量,占用量目标只会在前两个都满足后才会去考虑。

其他建议

首先建议系统默认选择,然后测试是否符合要求。

如果gc出现性能问题,首先考虑收集器是否和对应操作系统符合,尝试切换收集器。

可以通过工具来分析性能问题,根据分析结果考虑修改步骤。

–XX:+UseSerialGC

–XX:+UseParallelGC

–XX:+UseParallelOldGC

–XX:+UseConcMarkSweepGC

堆大小调整

1、  有足够大的内存

2、  堆不同分代的划分,串行收集时新生代不要超过一半。

并行收集调优策略

OutOfMemoryError

Gc找不到足够内存来分配对象,堆也不能再扩展了。

不一定是内存泄露了。

首先检查错误信息。

Java heap space.表明堆中没法分配空间了。调整堆大小。

PermGen space。永久区满了。调整永久区大小。

Requested array size exceeds vm limit。数组超限。

可使用HAT来分析。

评估GC性能的工具

-XX:+PrintGCDetails 会打印gc前后的各个分代存活对象信息,可用空间,GC耗时。

-XX:+PrintGCTimeStamps gc启动的时间戳。

Jmap/jstat/HPROF/HAT

GC的关键参数