2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > java虚拟机内存模型与垃圾回收知识复习总结

java虚拟机内存模型与垃圾回收知识复习总结

时间:2024-02-09 16:13:08

相关推荐

java虚拟机内存模型与垃圾回收知识复习总结

今天日子很特殊,1024程序员节,本来每个月计划的是至少一篇博客,刚好这个月还没写,今天的日志又特殊,必须要写一篇博客。

之前看过一些讲java虚拟机的课程,但是学过容易忘,总结一下,平时可以多看看。

内容有点多,先把几张图粘贴过来,内容以后慢慢添加吧!今天家里打来一个电话,心情比较烦闷,脑子很乱,所以文字内容等脑子清醒的时候,添加上去。

下图是java虚拟机的主要构成部分

再来一张图,介绍类加载机制

(1)加载:在硬盘上查找并通过IO读入字节码文件,将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。

(2)验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全(验证字节码的正确性)。

(3)准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int age= 18; 静态变量 age 就会在准备阶段被赋默认值0。

(4)解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用(将类的二进制数据中的符号引用换为直接引用)。

(5)初始化:类的初始化的主要工作是为静态变量赋程序设定的初值。

如static int age = 18; 在准备阶段,age被赋默认值0,在初始化阶段就会被赋值为18。

1.类的加载

(1)通过一个类的全限定名(包名加类名)来获取定义此类的二进制字节流(class文件)。而获取的方式,可以通过jar包,war包,网络中获取,jsp文件生成等方式。

(2)将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,这里只是转化了数据结构,并未合并数据。(方法去就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法去这个类的各种数据的访问入口。这个Class对象并没有规规定是在java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

2.类的初始化

类初始化时类加载的最后一步。

除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行java代码。

类的初始化的主要工作是为静态变量赋予程序设定的初值。如如static int age = 18 ; 在准备阶段,age被赋默认值0,在初始化阶段就会被赋值为18。

Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:

(1)使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。

(2)通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

(3)当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。

(4)当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。

(5)使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果。

虚拟机规范使用了“有且只有”这个词描述,这五种情况被称为“主动引用”。除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。

子类引用父类的静态变量

通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化

数组引用类

通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”情况,属于“被动引用”

静态常量引用类

静态常量在编译的阶段就会被存入到调用类的常量池中,不会引用到定义常量的类。这是一个特例,需要特别记忆,不会触发类的初始化!

类加载器

Java里有如下几种类加载器:

(1)启动类加载器(BootstrapClassloader):负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等。

(2)扩展类加载器(ExtClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。

(3)应用程序类加载器(AppClassLoader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。

(4)自定义加载器(CustomClassLoader):负责加载用户自定义路径下的类包。

为什么启动类加载器的打印为null?

启动类加载器采用native code实现,是JVM的一部分,主要加载JVM自身工作需要的类,如java.lang.*、java.uti.*等; 这些类位于$JAVA_HOME/jre/lib/rt.jar。Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

BootstrapClassLoader并不属于JVM的等级层次,它不遵守ClassLoader的加载规则,BootstrapClassLoader也没有子类。

双亲委派机制

双亲委派机制简单说来,就是先找父亲加载,不行的话再由儿子自己加载

为什么要设计双亲委派机制?

(1)沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。

(2)避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

沙箱安全机制,例如:

这说明了什么:找不到main方法,也就说明了我们类加载不是加载了我们应用中的String,而是JDK中的String了。

沙箱安全机制例子2

这说明了什么:找到了启动类加载器,启动加载器为native code。

Tomcat打破双亲委派机制分析

Tomcat使用默认类加载可行性分析?

问题:以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题

(1)一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

(2)部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

(3)web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

(4)web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?答案是不行的。为什么?

(1)第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

(2)第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

(3)第三个问题和第一个问题一样,不能实现。

(4)第四个问题,我们要怎么实现jsp文件的热加载? jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat自定义加载器打破双亲委派机制

tomcat的几个主要类加载器:

(1)commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;

(2)catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;

(3)sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;

(4)WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

打破双亲委派机制的实现

一、如何打破双亲委派机制

那我们分析下我们这个MyClassLoaderTest这个加载类的过程

二、自定义加载器打破双亲委派机制

举个栗子:MyClassLoaderTest2,我们通过重写loadClass()打破了双亲委派机制。可以作如下测试:

(1)在我们的应用中复制一个com.kfit.jvm.User1的类,

(1.1)然后运行,发现还是使用的 : MyClassLoaderTest2$MyClassLoader

(1.2)注释掉我们的上面的loadClass代码,在运行,那么结果是:Launcher$AppClassLoader

(2)尝试去加载:java.lang.String : 会报错:java.lang.SecurityException: Prohibited package name: java.lang

JVM整体结构及内存模型

栈(线程)

以Math类为例

JVM会为每个线程都分配一个栈的内存空间

一个方法对应一块栈帧内存区域

栈帧概念:局部变量、操作数栈、动态链接、方法出口

转换成更可读指令:javap -c Math.class

指令码对应表

程序计数器,对于内存模型不是独立的,需要整体理解

方法出口存储的就是方法执行完成之后需要返回的那个位置

引用类型变量:math

栈大小分析

前面我们对于栈有一个基本的认知了,栈核心的是有栈帧控制的,一个方法就会分配一个栈帧内存空间,这个栈空间,是否可以可以无限开辟栈帧内存空间,答案肯定是不可以的,想必大家都有碰到过栈内存溢出吧

java.lang.StackOverflowError

设置方式就是:JVM设置 -Xss160k ,默认的大小是1M ss:stack size

-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多

堆/栈/方法区的关系

关系梳理

(1)栈中的变量如果是引用类型的变量,那么会指向存放的对象的堆地址。

(2)堆中的对象的地址会指向元空间的对应的类元信息。

(3)元空间的静态变量会指向堆的地址。元空间在jdk1.8+之后,就直接存储在直接内存了,在以前是存放在JVM的一个持久代内存区域中的。

public static Useruser= new User();

这段代码user是一个静态变量,那么这个静态变量会存放到元空间中,对于右边的new User()是一个引用类型对象,会存储在堆中,所以user会指向堆的地址。

(4)程序计算器:当程序在执行线程指令的时候,会保存当前线程执行的位置。

本地方法栈

用过Thread的sleep方法吧:

public static native void sleep(long millis) throws InterruptedException

这个方法使用了native修饰,就是本地方法,底层使用的是c++语言进行编码的。对于这样的方法就会存在本地方法栈中了。

解析/静态链接/动态链接

解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用(将类的二进制数据中的符号引用换为直接引用)。

符号引用/静态链接/动态链接

符号引用/静态链接/动态链接

假定我们要第一次执行到math里调用compute()方法的那条invokevirtual指令了。此时JVM会发现该指令尚未被解析(resolve),所以会先去解析一下。通过其操作数所记录的常量池下标0x0004,找到常量池项#4,发现该常量池项也尚未被解析(resolve),于是进一步去解析一下。

invokevirtual #4 // Method compute:()I

这在Class文件中的实际编码为:[B6] [00 04]其中0xB6是invokevirtual指令的操作码(opcode),后面的0x0004是该指令的操作数(operand),用于指定要调用的目标方法。

invokevirtual = 182 (0xb6)

堆空间

堆内存概念

1.堆内存分两大块:年轻代(新生代)和老年代,它们的空间大小的比率是1:2,那么年轻代就是占了1/3,老年代占了2/3。 2.年轻代由两块区域组成:Eden和Survivor, E:S = 8:2 。 3.Survivor区由两块区域构成From(S0)区和To(S1)区,From:To = 1:1 4.Eden : From:To = 8:1:1 5.Young : Old = 1:2

举例说明:当我们给堆分配了3G大小的内存空间,那么:

Old区2/3 : 2G;Young区1/3:1G;

Eden区8/10 : 800M; From区1/10 : 100M ; To区1/10:100M。

堆对象处理过程分析

(1) 创建的对象会存放到堆的Eden区;

(2)当Eden区对象越来越多的时候,满的话,就会触发Minor GC,在Eden区存活的对象就会被移动到S0(From)区,Eden区被清空;

(3)等Eden区再满了,就再触发Minor GC,Eden区和S0(From)区存活的对象会被复制到S1(To)区,Eden区被清空,From区被清空;

(4)Eden区再满的话,就再触发Minor GC,Eden区和S1(To)区存活的对象会被复制到S0(From)区,Eden区被清空,To区被清空。

(5)如此循环往复,经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么除了Eden区,还要设置两个Survivor区

为什么要有Survivor

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。

可以得到第一条结论:Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么要设置两个Survivor

假设现在只有一个survivor区,我们来模拟一下流程:

刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

碎片化带来的风险是极大的,严重影响Java程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存。

那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到15次,该对象就会被送到老年代中。

上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

那么,Survivor为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满,因此,我认为两块Survivor区是经过权衡之后的最佳方案。

JVM

内存参数设置

1Xss:

含义:设置线程栈大小,默认值为1M;

辅助记忆:ss -> stack size

-X:非标准参数。这些参数不是虚拟机规范规定的。因此,不是所有VM的实现(如:HotSpot,JRockit,J9等)都支持这些配置参数。

-XX:不稳定参数。这些参数是虚拟机规范中规定的。这些参数指定虚拟机实例在运行时的各种行为,从而对虚拟机的运行时性能有很大影响。

2)方法区:

关于元空间的JVM参数有两个:

-XX:MetaspaceSize=N

-XX:MaxMetaspaceSize=N

对于64位JVM来说,元空间的默认初始大小是21MB,默认的元空间的最大值是无限。MaxMetaspaceSize用于设置metaspace区域的最大值。

Jdk1.6及之前: 有永久代, 常量池在方法区

Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池在堆

Jdk1.8及之后: 无永久代,常量池在元空间

3)堆:

-Xms:minimum memory size for pile and heap : 设置JVM最大可用内存

-Xmx:maximum memory size for pile and heap:设置JVM促使内存

Jdk1.8之前:整个JVM内存大小=年轻代大小+老年代大小+持久代大小。

Jdk1.8+:整个JVM内存大小=年轻代大小+老年代大小。

-Xmn:设置新生代的大小。

设置举例

Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):

java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar mypreoject.jar

垃圾收集算法

如何判断对象可以被回收

一、引用计数法ReferenceCountingGc

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

二、可达性分析算法

将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。

GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

三、常见引用类型

3.1强引用:普通的变量引用

public static Useruser= new User();

3.2软引用

浏览器的后退按钮

将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。

3.3弱引用

将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用。

public static WeakReference<User>user= new WeakReference<User>(new User());

3.4虚引用

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

public static PhantomReference<User>user= new PhantomReference(new User(),new ReferenceQueue<>());

何判断对象可以被回收(二)

四、finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

(1)第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行finalize()方法。

当对象没有覆盖finalize方法,对象将直接被回收。

(2)第二次标记

如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

垃圾回收算法

一、标记-清除算法

算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法最基础的收集算法,效率也很高,但是会带来两个明显的问题:

(1)效率问题 : 相对于复制算法而言。

(2)空间问题(标记清除后会产生大量不连续的碎片):大对象就不能进行保存了。

二、复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

三、标记-整理算法

根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。

四、分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象( 近99% )死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。

而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上

垃圾收集器(一)

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

一、Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法。

二、ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。默认的收集线程数跟CPU核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

新生代采用复制算法,老年代采用标记-整理算法。

三、Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

Parallel Scavenge 收集器类似于ParNew 收集器,是Server 模式(内存大于2G,2个cpu)下的默认收集器,那么它有什么特别之处呢?

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,比如设置STW的时间,这样的话,就会导致垃圾一次性无法收集完。

新生代采用复制算法,老年代采用标记-整理算法。

垃圾收集器(二)

四、CMS收集器(-XX:+UseConcMarkSweepGC『old:只能用在老年代 』)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。

整个过程分为五个步骤:

(1)初始标记: 在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。

(2)并发标记: 这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

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

(4)并发清理: 清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

(5)并发重置:这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

优点:并发收集 低停顿

缺点:

(1)对CPU资源敏感(会和服务抢资源);

(2)无法处理浮动垃圾 (在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了 );

(4)它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数 -XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理。

(5)执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发Full GC,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收。

CMS相关参数:

-XX:+UseConcMarkSweepGC:启用CMS ;

-XX:ConcGCThreads:并发的GC线程数;

-XX:+UseCMSCompactAtFullCollection:Full GC之后做压缩整理(减少碎片);

-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次 ;

-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比);

-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整;

-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor GC,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段;

-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的多线程执行,减少STW

-XX:+CMSParallelRemarkEnabled:在重新标记的时候多程执行,降低STW;

五、G1收集器(-XX:+UseG1GC)

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

面对大容量内存的机器

G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。

一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。

G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。

Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。

Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1,假设年轻代现在有1000个region,Eden区对应800个,s0对应100个,s1对应100个。

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。

G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。

运行过程

(1)初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;

(2)并发标记(Concurrent Marking):同CMS的并发标记

(3)最终标记(Remark,STW):同CMS的重新标记

(4)筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。

参数配置

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads: 指定GC工作的线程数量

-XX:G1HeapRegionSize : 指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区

-XX:MaxGCPauseMillis: 目标暂停时间(默认200ms)

-XX:G1NewSizePercent: 新生代内存初始空间 ( 默认整堆5%)

-XX:G1MaxNewSizePercent: 新生代内存最大空间

-XX:TargetSurvivorRatio: Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代

-XX:MaxTenuringThreshold: 最大年龄阈值( 默认15 )

-XX:InitiatingHeapOccupancyPercent: 老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了

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

-XX:G1MixedGCLiveThresholdPercent :(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。

-XX:G1MixedGCCountTarget: 在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。

G1垃圾收集分类

YoungGC

YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC。

MixedGC

不是Full GC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen)设定的值则触发,回收所有的Young和部分Old( 根据期望的GC停顿时间确定old区垃圾收集的优先顺序 ) 以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。

FullGC

停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。

G1垃圾收集器优化建议

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。

那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。

或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。

所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发Mixed GC。

什么场景适合使用G1

(1)50%以上的堆被存活对象占用

(2)对象分配和晋升的速度变化非常大

(3)垃圾回收时间特别长,超过1秒

(4)8GB以上的堆内存(建议值)

(5)停顿时间是500ms以内

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。