JVM内存模型及三种GC的详解

xiaoxiao2021-04-15  83

先上个图

再聊聊JVM的年轻代

1.为什么会有年轻代

我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

2.年轻代中的GC

    HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

 

3.一个对象的这一辈子

我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

4.有关年轻代的JVM参数

1)-XX:NewSize和-XX:MaxNewSize

      用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

      用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution

      这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

      用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

 

(以下段落基于JDK6)

说到GC,首先要对Java 的内存模型有所了解。

Java 的内存模型各个代的默认排列有如下图(适用JDK1.4.*  到 JDK6):

Java 的内存模型分为

Young(年轻代)

Tenured(终身代)

Perm(永久代)

 

更多关于内存模型的文章看这里:

图解JVM在内存中申请对象及垃圾回收流程

图解JVM内存模型

 

在堆内存中的GC可以分为Minor GC(次要GC)和 Major GC(主要GC),次要GC是在年轻代进行收集的GC,职责是在Eden区满的时候收集dead的对象和转移存活的对象;主要GC是在终生代满时进行收集的GC,主要GC较次要GC需要更多时间。 使用jvm参数-verbose:gc 就可以输出每一次GC的详细信息。

你可能在程序运行起来以后看到如下输出:

 

[GC 325407K->83000K(776768K), 0.2300771 secs] [GC 325816K->83372K(776768K), 0.2454258 secs] [Full GC 267628K->83769K(776768K), 1.8479984 secs]

你可以看到两次Minor GC(次要GC)和一次Major GC(主要GC)。

325407K->83000K   箭头前后的数字分别代表收集前和收集后的堆内存占用情况。 (776768K) 括号内的数字代表总共分配的堆内存空间,注意,这个值不包括其中一个Survior空间,也不包括 permanent generation(永久代)。

最后的时间0.2300771 secs指的是GC所耗费的时间。

 

如果运行时加上VM参数-XX:+PrintGCDetails    将输出更详细的信息。如下显示了Eden区和Heap内存在GC前后的变化:

 

[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]

 

如果运行时加上VM参数-XX:+PrintGCTimeStamps 则可以得到GC发生的时间。

以下输出显示了在程序运行到111.042 秒的时候发生的GC,包括一次在Eden区的次要GC和发生在Tenured区的主要GC:

 

111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505 secs]111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs] 26282K->2311K(32704K), 0.1293306 secs]

 

GC性能主要的衡量指标有两个:Throughput和Pauses。吞吐量(Throughput)是不做GC的时间与总时间的百分比,分子包括分配内存空间的时间。中断(Pauses)是测量时间段内由于GC而导致的应用暂停次数。 对用户而言,对GC的需求往往是不一样的。一般的web应用对吞吐量要求不高,由于GC而引起的偶尔中断也是可以容忍的。然而一个交互性强的实时应用系统来说,经常性的中断将带来糟糕的用户体验。 即时性(Promptness)和足印(footprint)也是某些用户考虑的问题。 即时性是对象死去到所占内存释放的时间间隔,这个指数是分布式应用如使用RMI的分布式应用的一项需要考虑的因素。足印是一种过程的集合,代表可伸缩性。

 

 

HOTSPOT JVM总共拥有3种不同的GC,各有各总自的特点和应用场景:

 

serial collector (串行GC)任何时刻都是使用一个线程执行GC操作,这种GC在线程间通信没有大的开销的应用会有相对不错的运行效率。最适合单处理器的系统;多处理器系统对这种GC而言并不能提升收集的效率。JVM默认情况下就是使用这个GC,这种GC有个形象的别名叫做"stop-the-world",当JVM在用这个GC收集垃圾的时候,你的app别想干其他事。你也可以用这个参数 -XX:+UseSerialGC 显式的声明使用。 默认的serial gc可以应付绝大多数的app。除非以下情况:这是一个运行在大内存多处理器的机器上的多线程的大应用。 parallel collector (并行GC,或者叫 throughput collector ) 会以并行的方式运行minor collections(次级GC), 能较大的减少GC的开销。其诞生的初衷就是专门给运行在多处理器,多线程硬件上的中大型应用的。在特定的硬件和OS环境条件下这是默认选项,显式声明使用 -XX:+UseParallelGC 参数。始于JDK1.3.1。 parallel compaction 是在 J2SE 5.0 update 6引入的新特性,并在Java SE 6 得到增强。它允许使用并行的方式运行Major collections(主要GC)。如果不开启parallel compaction, major collections 将以单一线程的方式运行。 通过参数 -XX:+UseParallelOldGC 显式使用该特性。 concurrent collector (同步GC)同时运行大多数的任务 (GC的同时应用也在运行)来保证GC引起的中断时间尽量的短。主要应用在实时性要求重于总体吞吐量要求的中大型应用,即使如此,降低中断时间的技术还是会导致应用程序性能的少许降低。可以使用参数 -XX:+UseConcMarkSweepGC 使用该特性。

 

parallel collector的一些注意点:

parallel collector从JDK5开始就是Server端JVM的默认选择,需要加VM参数 -server 。

parallel collector还采用一些细节调优的策略如:

限定最大GC中断时间吞吐量限定足印(伸缩量)设定

可以用vm参数 -XX:MaxGCPauseMillis=<N> 来限定最大GC中断时间,单位是ms,规定GC产生的中断时间不能超过指定的时间。默认没有这个限制。一旦使用了这个参数,heap空间和其他相关参数会做出相应的调整来满足最大GC中断时间的要求。

吞吐量限定使用-XX:GCTimeRatio=<N> 来设定GC时间的比率,N 的值 = 没有花在GC上的时间/GC的时间 因此GC的时间占用总时间的百分比公式= 1 / (1 + <N>) 。比如 -XX:GCTimeRatio=19 意味着将有1/20的时间花在GC上。默认值=99。

 

足印(伸缩量)实际上就是heap堆内存的调整。最大Heap容量使用参数 -Xmx<N> 声明。

 

以上参数中任何一个的改动,都会引起另外两个的改变。三者的优先级如上顺序一至。

如果太多时间黑白花费在GC上,parallel collector将抛出OOM,这临界值大概是98%;也可以使用-XX:-UseGCOverheadLimit 关闭这个特性。

 

concurrent collector的一些注意点:

 

不适用于单处理器的系统,事实上在单处理器系统上运行concurrent collector 效率反而降低。如果只能运行在单处理器的系统上,那记得开启增量模式(incremental mode)。

前面几种GC都是在Tenured区满了以后触发主要GC操作;concurrent collector却是在Tenured区满溢之前就进行主要GC。如果concurrent collector没有赶在Tenured区满前收集完或者还没有开始收集的话,就会产生长时间的中断。参数-XX:CMSInitiatingOccupancyFraction=<N> 可以指定触发主要GC的临界值,N(0-100)代表的是Tenured区饱和程度百分比。一旦Tenured区饱和程度达到这个临界值,主要GC就发生了。

 

concurrent collection的生命周期一般包括如下几步:

停止应用的所有线程,标识出所有可到达的对象集合,然后恢复应用的所有线程使用一个或几个处理器资源同步跟踪可到达的对象,应用线程同时运行使用一个处理器资源同步地重新定位那些自从上一个步骤以来可能修改过的对象停止应用的所有线程,重新定位那些自从上一个步骤以来可能修改过的对象,然后恢复应用的所有线程使用一个处理器资源同步地收集不被引用的对象使用一个处理器资源同步地重新定义堆内存,并且为下一次GC生命周期作好数据准备

concurrent collection在整个收集的过程中,至少会占有一到两个处理器,而且不会自动放弃占有的处理器资源。

这个特性会让只有一到两个处理器的系统很难过。为处理这个问题,需要借助增量模式(incremental mode)。

 

增量模式 的核心思想是 将整个GC生命周期分解成一段段的时间块分步进行,以此减少中断的时间,但是不可避免的是伴随着吞吐量的下降。

使用参数 -XX:+CMSIncrementalMode 开启增量模式。

转载请注明原文地址: https://www.6miu.com/read-4817409.html

最新回复(0)