JVM内存区域与JVM优化,看了这一篇,能帮你更好理解

一 JVM中的类加载机制与双亲委派机制

1.1 类加载的过程

1 加载阶段

2 验证阶段

3 准备阶段

4 解析阶段

5 初始化阶段

1.2 什么时候会触发初始化

1 调用构造方法初始化类的实例,会加载和初始化该类。

2 初始化某个类之前,发现该类的父类还没有加载和初始化,那么先加载这个类的父类,然后进行初始化操作。

3 包含"main()"方法的主类,必须立马初始化。

1.3 类加载器

Bootstrap ClassLoader

负责加载我们在机器上安装的Java目录下的核心类的。在Java安装目录下,就有一个“lib”目录,这里就有Java最核心的一些类库,支撑你的Java系统的运行。所以一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。

Extenssion ClassLoader

在Java安装目录下有一个"lib/ext"目录,该目录下有一些类,需要使用Extenssion ClassLoader类加载器来加载该目录下的类,支撑你的系统的运行。

Application ClassLoader

这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类(这个类加载器就负责加载你写好的那些类到内存里

1.4 双亲委派机制

应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载但是如果父类加载器在自己负责加载的范围内没找到这个类,就会下推加载权利给自己的子类加载器加载,然后应用程序类加载器在自己负责的范围内,将这个类加载到内存中。

二 JVM中的内存区域

JVM的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)和直接内存。

  1. 线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。

  1. 线程共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁。

  1. 直接内存也叫作堆外内存,它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。

1.1 程序计数器

程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的号指示器。每个运行中的线程都有一个独立的程序计数器,在方法正在执行时, 该方法的程序计数器记录的是实时虚拟机字节码指令的地址:如果该方法执行的是Native方法,则程序计数器的值为空( Undefined )。程序计数器属于“线程私有”的内存区域,它是唯一没有内存溢出区域。

1.2 虚拟机栈

虚拟机栈是描述Java 方法的执行过程的内存模型,它在当前栈帧中存储了局部变量表、操作数栈、动态链接、方法出口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理动态链接方法的返回值和异常分派。

1.3 本地方法区

本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java 方法服务。本地方法栈为Native方法服务。

1.4 堆

在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域。由于现代JVM采用分代收集算法,因此Java堆从GC的角度还可以细分为:新生代、老年代和永久代。

1.5 方法区

方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据。

JVM把GC分代收集扩展至方法区、即使用Java 堆的永久代来实现方法区,这样JVM的垃圾收集器就可以像管理Java堆一样管理这部分内存。 永久带的内存回收主要针对常量池的回收和类的卸载,因此可回收的对象很少。

三 JVM中内存分代模型

Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

3.1 新生代

是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

3.1.1 Eden区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

3.1.1.2 ServivorFrom

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

3.1.1.3 ServivorTo

保留了一次 MinorGC 过程中的幸存者。

3.1.1.4 MinorGC 的过程(复制->清空->互换)

MinorGC 采用复制算法。

1:eden、servicorFrom 复制到 ServicorTo,年龄+1

首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);

2:清空 eden、servicorFrom

然后,清空 Eden 和 ServicorFrom 中的对象;

3:ServicorTo 和 ServicorFrom 互换

最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。

3.2 老年代

主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

3.3 永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

3.3.1 JAVA8 与元数据

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

四 JVM内存相关的几个核心参数

4.1 核心参数介绍

-Xms和-Xmx,分别用于设置Java堆内存的刚开始的大小,以及允许扩张到的最大大小对于这对参数,通常来说,都会设置为完全一样的大小。

-Xmn,用来设置Java堆内存中的新生代的大小,然后扣除新生代大小之后的剩余内存就是给老年代。

-XX:PermSize 和 -XX:MaxPermSize,分别限定了永久代初始化大小和永久代的最大的大小。

-Xss,这个参数限定了每个线程的栈内存大小。

4.2 启动程序时指定参数大小

4.2.1 在IDEA启动程序时指定内存的大小

4.2.2 使用java指定运行jar包时指定内存大小

java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -jar PmsApplication.jar

五 JVM中的垃圾回收机制

在JAVA开发中,我们会不断创建很多的对象,这些对象数据会占用系统内存,如果得不到有效的管理,内存的占用会越来越多,甚至会出现内存溢出的情况,所以,我们需要进行对内存进行合理地释放,这个时候 GC 就派上大用场的。

垃圾回收(GC)是由 Java 虚拟机(JVM)垃圾回收器提供的一种对内存回收的一种机制,它一般会在内存空闲或者内存占用过高的时候对那些没有任何引用的对象不定时地进行回收。

5.1 如何确定要回收的垃圾

5.1.1 引用计数法

一个对象被创建之后,系统会给这个对象初始化一个引用计数器,当这个对象被引用了,则计数器 +1,而当该引用失效后,计数器便 -1,直到计数器为 0,意味着该对象不再被使用了,则可以将其进行回收了。

这种算法其实很好用,判定比较简单,效率也很高,但是却有一个很致命的缺点,就是它无法避免循环引用,即两个对象之间循环引用的时候,各自的计数器始终不会变成 0,所以 引用计数算法 只出现在了早期的 JVM 中,现在基本不再使用了。

5.1.2 可达性分析法

为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。

要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

5.2 什么是GC roots

一个对象被局部变量引用或者被静态变量引用,那么就说明他有一个GC Roots,此时就不能被回收了。

5.3 JAVA中的几种引用类型

强引用(Strong Reference)

例如:Object obj =new Object();

上述Object这类对象就具有强引用,属于不可回收的资源,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠回收具有强引用的对象,来解决内存不足的问题。

软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用(Weak Reference)

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

幻象引用/虚引用(Phantom References)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

几种引用类型的区别

有GC Roots引用的对象,如果是强引用是不能回收的;如果是软引用内存实在不够用了可以被回收掉;

如果是弱引用,对象可以被回收掉。

没有GC Roots引用的对象可以回收。

5.4 finalize方法

finalize()方法是对象逃脱死亡命运的最后一次机会。假设有一个ReplicaManager对象要被垃圾回收了,此时这个对象重写了Object类中的finialize()方法,此时会先尝试调用一下他的finalize()方法,看是否把自己这个实例对象给了某个GC Roots变量,比如说代码中就给了ReplicaManagel类的静态变量。如果重新让某个GC Roots变量引用了自己,那么就不用被垃圾回收了

六 新生代垃圾回收算法

所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。

新生代内存按照8:1:1的比例分为一个 eden区和两个survivor(survivor0,survivor1) 区。大部分对象在Eden区中生成,回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区。当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的然后将survivor0区和survivor1区交换,即保持survivor1区为空,如此往复。

当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

七 老年代的垃圾回收算法

7.1 哪些对象可以进入到老年代?

7.1.1 躲过15次Minor GC的对象会进入到老年代中。

可以通过VM参数“-XX:MaxTenuringThreshold”来设置默认是15岁。

7.1.2 动态年龄判断

假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代。

规则逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄大于等于n的对象都放入老年代。

假设这个图里的Survivor2区有多个对象,从年龄1开始累加对象的大小到年龄3时,加起来的对象超过了50MB,超过了Survivor2区的100MB内存大小的一半,这个时候,Survivor2区里的大于等于3岁的对象,就要全部进入老年代里去。

7.1.3 大对象直接进入老年代

有一个JVM参数,就是“-XX:PretenureSizeThreshold”可以把他的值设置为字节数,比如“1048576”字节,就是1MB。他的意思就是,如果你要创建一个大于这个大小的对象,此时就直接把这个大对象放到老年代里去。压根儿不会经过新生代。之所以这么做,就是要避免新生代里出现那种大对象,然后屡次躲过GC,还得把他在两个Survivor区域里来回复制多次之后才能进入老年代那么大的一个对象在内存里来回复制,不是很耗费时间吗 ?所以说,这也是一个对象进入老年代的规则。

7.1.4 MoniorGC后的对象太多无法放入Survor区域

假设在发生Monio GC的时候,发现Eden区里超过150MB的存活对象,此时没办法放入Survivor区中, 这个时候就必须得把这些对象直接转移到老年代中

7.1.5 老年代空间分配担保规则

如果新生代里有大量对象存活下来,确实是自己的Survivor区放不下了,必须转移到老年代中。那么如果老年代里空间也不够放这些对象呢?这该咋整?

首先,在执行任何一次Minor GC之前,JVM会先检查一下老年代可用的内存空间,是否大于新生代所有对象的总大小。如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆 的对新生代发起一次Minor GC了,因为即使Minor GC之后所有对象都存活,Survivor区放不下了,也可以转移到老年代去

但是假如执行Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小。就会看是否设置了“-XX:-HandlePromotionFailure”的参数,如果有这个参数,那么就会继续尝试进行下一步判断,看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。之前每次Minor GC后,平均都有10MB左右的对象会进入老年代,那么此时老年代可用内存大于10MB。这就说明,很可能这次Minor GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的,此时触发年轻代的Monior GC。

此时进行MoniorGC的几种可能:

第一种可能,Minor G过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可。

第二种可能,Minor GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可。

第三种可能,很不幸,Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle PromotionFailure”的情况,这个时候就会触发一次“Full GC”。

如果老年代内存大小与每次Monior GC的平均值进行比较,发现小于Monior GC的平均值,或者是没设置“-XX:-HandlePromotionFailure”参数,此时就会直接发一次“Full GC",就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC。

如果要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的“OOM”内存溢出了。

7.2 老年代的垃圾回收算法

7.2.1 垃圾回收的时机:

1 在Minor GC之前,检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需要提前触发Full GC然后再带着进行Minor GC

2 在Minor GC之后,发现剩余对象太多放入老年代都放不下了

7.2.2 老年代采取的垃圾回收算法

简单来说,老年代采取的是标记整理算法,这个过程说起来比较简单,首先标记出来老年代当前存活的对象,这些对象可能是东一个西一个的。

7.3 stop the word问题

stop the world指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应, 有点像卡死的感觉,这个停顿称为STW。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

八 年轻代垃圾回收器ParNew与Serial

8.1 ParNew与Serial简介

在没有最新的G1垃圾回收器之前,通常大家线上系统都是ParNew垃圾回收器作为新生代的垃圾回收器。通常运行在服务器上的Java系统,都可以充分利用服务器的多核CPU的优势。所以大家可以想一下,假设你的服务器是4核CPU,理论上4核CPU就可以支持4个垃圾回收线程并行执行,比单线程的方式性能提高了4倍。另外一种Serial垃圾回收器主打的是单线程垃圾回收,他们俩都是回收新生代的,唯一的区别就是单线程和多线程的区别,垃圾回收算法是完全一样的。

Minor GC的时机,检查机制,包括垃圾回收的具体过程,以及对象升入老年代的机制,都是我们之前说过的那套原理。

8.2 如何为线上系统指定使用ParNew垃圾回收器

很简单,使用“-XX:+UseParNewGC”选项,只要加入这个选项,JVM启动之后对新生代进行垃圾回收的,就是ParNew垃圾回收器了。

8.3 指定ParNew垃圾回收器默认个情况下的的线程数量

一般我们指定了使用ParNew垃圾回收器之后,他默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的。比如我们线上机器假设用的是4核CPU,或者8核CPU,或者16核CPU,那么此时ParNew的垃圾回收线程数就会分别是4个线程、8个线程、16个线程。但是如果你一定要自己调节ParNew的垃圾回收线程数量,也是可以的,使用“-XX:ParallelGCThreads”参数即可通过他可以设置线程的数量,但是建议一般不要随意动这个参数

九 老年代垃圾回收器CMS

9.1 老年代执行一次垃圾回收的过程

9.1.1 初始标记

首先,CMS要进行垃圾回收时,会先执行初始标记阶段,这个阶段会让系统的工作线程全部停止,进入“Stop the World”状态。所谓的“初始标记,标记出来所有GC Roots直接引用的对象。方法的局部变量和类的静态变量是GC Roots。但是类的实例变量不是GC Roots。

9.1.2 并发标记

同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

9.1.3 重新标记

重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。

9.1.4 并发清除

开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

十 G1分代回收

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

10.1 ParNew + CMS的组合的最大的痛点

Stop the World.

无论是新生代垃圾回收,还是老年代垃圾回收,都会或多或少产生“Stop the World”现象,对系统的运行是有一定影响的。所以其实之后对垃圾回收器的优化,都是朝着减少“Stop the World”的目标去做的。在这个基础之上,G1垃圾回收器就应运而生了,他可以提供比“ParNew + CMS”组合更好的垃圾回收的性能

10.2 G1垃圾回收器

G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,他一个人就可以搞定所有的垃圾回收。他最大的一个特点,就是把Java堆内存拆分为多个大小相等的Region。

然后G1也会有新生代和老年代的概念,但是只不过是逻辑上的概念。也就是说,新生代可能包含了某些Region,老年代可能包含了某些Reigon。

而且G1最大的一个特点,就是可以让我们设置一个垃圾回收的预期停顿时间。

也就是说比如我们可以指定:希望G1同志在垃圾回收的时候,可以保证,在1小时内由G1垃圾回收导致的“Stop the World”时间也就是系统停顿的时间,不能超过1分钟。

之前我们的很多JVM优化的思路,都明白一点,其实我们对内存合理分配,优化一些参数,就是为了尽可能减少Minor GC和Full GC,尽量减少GC带来的系统停顿,避免影响系统处理请求,但是现在我们直接可以给G1指定,在一个时间内,垃圾回收导致的系统停顿时间不能超过多久,G1全权给你负责,保证达到这个目标。

10.3 G1垃圾回收器如何做到对垃圾回收导致的系统停顿可控

其实G1如果要做到这一点,他就必须要追踪每个Region里的回收价值,啥叫做回收价值呢?

他必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?大家看下图,G1通过追踪发现,1个Recion中的垃圾对象有10MB,回收他们需要耗费1秒钟,另外一个Region中的垃圾对象有20MB,回收他们需要耗费200毫秒。

然后在垃圾回收的时候,G1会发现在最近一个时间段内,比如1小时内,垃圾回收已经导致了几百毫秒的系统停顿了,现在又要执行次垃圾回收,那么必须是回收上图中那个只需要200ms就能回收掉20MB垃圾的Region啊!于是G1触发一次垃圾回收,虽然可能导致系统停顿了200ms,但是一下子回收了更多的垃圾,就是20MB的垃圾。

总结G1的设计思路

G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个 Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影 响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。

10.4 Region可能属于新生代可能属于老年代

另外在G1中,每一个Region可能属于新生代,但是也可能属于老年代的。刚开始Region可能谁都不属于,然后接着就分配给了新生代,然后放了很多属于新生代的对象,接着就触发了垃圾回收这个Region。然后下一次同一个Region可能又被分配了老年代了,用来放老年代的长生存周期的对象

所以其实在G1对应的内存模型中,Region随时会属于新生代也会属于老年代,所以没有所谓新生代给多少内存,老年代给多少内存这说了。实际上新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。

10.5 如何设置G1对应的内存大小

那么首先思考两个问题:到底有多少个Region呢?每个Region的大小是多大呢?

其实这个默认情况下自动计算和设置的,我们可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。然后JVM启动的时候 一旦发现你使用的是G1垃圾回收器,可以使用“-

XX:+UseG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048。因为JVM最多可以有2048个 Region。比如说堆大小是4G,那么就是4096MB,此时除以2048个Region,每个Region的大小就是 2MB。如果通过手动方式来指定,则是“-XX:G1HeapRegionSize”

刚开始的时候,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个是可以通过-XX:G1NewSizePercent”来设置新生代初始占比的,使用默认值即可。因为 在系统运行中,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%, 可以通过“-XX:G1MaxNewSizePercent"。而且一旦Region进行了垃圾回收,此时新生代的Region数 量还会减少。

10.6 G1垃圾回收器中的Eden区和Survivor区

在新生代中通过设置参数“-XX:SurvivorRatio=8”设置比例。 比如新生代之前说刚开始初始的时候,有100个Region,那么可能80个Region就是Eden,两Survivor各自占10个Region。随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。

10.7 G1新生代的垃圾回收

随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%

这个时候还是会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收,进入一个“Stop theWorld”状态。然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden中对应的Region中的垃圾对象。但是这个过程跟之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms。

那么G1就会通过之前说的,对每个Region追踪回收他需要多少时间,可以回收多少对象来选择回收一部分的Region,保证GC停顿时间控制在指定范围内,尽可能多的回收掉一些对象。

10.8 哪些对象进入老年代中

大家都知道,在G1的内存模型下,新生代和老年代各自都会占据一定的Region,老年代也会有自己的Region。按照默认新生代最多只能占据堆内存60%的Region来推算,老年代最多可以占据40%的Region,大概就是800个左右的Region。那么对象什么时候从新生代进入老年代呢?

可以说跟之前几乎是一样的,还是这么几个条件:

(1)对象在新生代过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,他就会进入老年代。

(2)动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%。(如果Survivor区域内年龄1+年龄2+年龄3+年龄n的对象总和大于Survivor区的50%,此时年龄n以上的对象会进入老年代)

(3)大对象,实际上这里会有所改变,G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中。而且一个大对象如果太大,可能会横跨多个Region来存放。

补充

(1)那堆内存里哪些Region用来存放大对象啊?不是说60%的给新生代,40%的给老年代吗,那还有哪些Region给大对象?

很简单,之前说过了,在G1里,新生代和老年代的Region是不停的变化的。比如新生代现在占据了1200个Region,但是一次垃圾回收之后,就让里面1000个Region都空了,此时那1000个Region就可以不属于新生代了,里面很多Region可以用来存放大对象。

(2)大对象既然不属于新生代和老年代,那么什么时候会触发垃圾回收呢?

其实新生代,老年代在回收的时候,会顺带带着大对象Region一起回收,所以这就是在G1内存模型下对大对象的分配和回收的策略。

十一 G1老年代混合回收过程 以及G1的一些参数

11.1 什么时候触发新生代+老年代的混合垃圾回收

G1有一个参数,是“-XX:lnitiatingHeapOccupancyPercent”他的默认值是45%。意思就是说,如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段,

比如按照我们之前说的,堆内存有2048个Region ,如果老年代占据了其中45%的Region,也就是接近1000个Reqion的时候,就会开始触发一个混合回收。

11.2 G1老年代混合回收过程

初始化标记

首先会触发一个“初始标记”的操作,这个过程是需要进入“Stop the World”的,仅仅只是标记一下GCRoots直接能引用的对象这个过程速度是很快的。如下图,先停止系统程序的运行,然后对各个线程栈内存中的局部变量代表的GC Roots,以及方法区中的类静态变量代表的GCRoots,进行扫描,标记出来他们直接引用的那些对象

并发标记

接着会进入"并发标记”的阶段,这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GCRoots开始追踪所有的存活对象。如下图所示

这里对GCRoots追踪做更加详细的说明,比如下面的代码。大家可以看到,Kafka这个类有一个静态变量是“replicaManager”,他就是一个GC Root对象,初始标记阶段,仅仅就是标记这"replicaManager”。作为GC Roots直接关联的对象,就是“ReplicaManager”对象,他肯定是要存活的。然后在并发标记阶段,就会进行GC Roots追踪,会从“replicaManager”这个GC Root对象直接关联的“ReplicaManager”对象开始往下追踪。可以看到“ReplicasManager”对象里有一个实例变量"replicaFetcher”,此时追踪这个“replicafetcher”变量可以看到他引用了ReplicaFetcher”对象,那么此时这个“ReplicaFetcher”对象也要被标记为存活对象

这个并发标记阶段还是很耗时的,因为要追踪全部的存活对象。但是这个阶段是可以跟系统程序并发运行的,所以对系统程序的影响不太大,而目JM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。接着是下一个阶段,最终标记阶段,这个阶段会进入“Stop the world”,系统程序是禁止运行的,但是会根据并发标记阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象,如下图所示。

"混合回收"阶段

这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。比如说老年代此时有1000个Region都满了,但是因为根据预定目标,本次垃圾回收可能只能停顿200毫秒,那么通讨之前的计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,把GC导致的停顿时间控制在我们指定的范围内,如下图。

而且大家需要在这里有一点认识,其实老年代对堆内存占比达到45%的时候,触发的是“混合回收"。也就是说,此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象。

那么,到底是回收这些区域的哪些Region呢?

那就要看情况了,因为我们设定了对GC停时间的目标,所以说他会从新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收。

11.3 G1垃圾回收器的一些参数

-XX:G1MixedGCCountTarget

大家在上面都看到了,一般在老年代的Reqion占据了堆内存的Reion的45%之后,会触发混合回收就是Mixed GC。混合回收分为了好几个阶段,在最后一个环节,从新生代和老年代里都回收一些Region。但是最后一个阶段混合回收的时候,其实会停止所有程序运行。G1是允许执行多次混合回收,比如先停止工作,执行一次混合回收回收掉一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉些Region。“-XX:G1MixedGCCountTarget”参数控制的是,在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次。这意味着最后一个阶段,先停止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Reqion,反复8次。那么为什么要反复回收多次呢? 因为你停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。

-XX:G1HeapWastePercent

在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉。这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混 合回收就结束了。

-XX:G1MixedGCLiveThresholdPercent

意思是确定要回收Region的时候,必须是存活对象低于85%的Region才可以进行回收。否则要是一个Region的存活对象多余85%,你还回收他干什么?这个时候要把85%的对象都拷贝到别的Reion,这个成本是很高的。

11.4 回收失败的Full GC

如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去。此时万一出现拷贝的过程中发现没有空闲Reion 可以承载自己的存活对象了,就会触发一次失败。一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程很慢。

第一次写文章,如有不足,多多包涵

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值