一、JVM内存模型
1、名词解释
堆: 其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。包含:新生代(Eden区、S0、S1)、老年代。官方推荐配置为年轻代大小占整个堆的3/8。-XX:NewRatio=3/5表示新生代和老年代的比值, 而Eden:S0:S1=8:1:1
虚拟机栈:描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储 局部变量、操作数栈、动态链接、方法出口等信息.生命周期与线程相同。栈里面存放着各种基本数据类型和对象的引用
如果在[栈帧]中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的 对象
方法区(元空间):存储已被虚拟机加载的类信息。jdk8的JVM不再有永久代(PermGen),原永久代存储的信息被分成两部分:
1、虚拟机加载的类信息(放在元空间)
2、运行时常量池(放在堆中)
方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。private static Object obj=new Object();
本地方法栈:本地方法栈则为虚拟机使用到的Native方法服务(非java代码的接口,比如C++的方法:Runtime.getRuntime().exec()是执行shell脚本的命令)
程序计数器:当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响
二、垃圾回收
查看当前JVM默认的垃圾回收器
java -XX:+PrintCommandLineFlags -version
结果如下
-XX:InitialHeapSize=63730944
-XX:MaxHeapSize=1019695104
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseParallelGC
java version "1.8.0_351"
Java(TM) SE Runtime Environment (build 1.8.0_351-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.351-b10, mixed mode)
结果分析:由结果可以看出Java8的GC情况是:-XX:+UseParallelGC,即Parallel Scavenge(新生代) + Parallel Old
指定老年代CMS垃圾回收器
-XX:+UseConcMarkSweepGC
如何判断一个对象是否应该被回收
怎么样判断一个对象是否会被垃圾回收器回收,释放内存。分别有可达性分析法和引用计数法
可达性分析
判断一个对象是否可达,不可达对象就将被回收,所谓可达就是从GCROOT开始是否是可以找到该对象 GCROOT是什么?
1、虚拟机栈中引用的对象(本地变量表)
2、方法区中静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中引用的对象(Native Object)
引用计数法
在JDK8中,Java并没有采用引用计数法来进行垃圾回收。引用计数法是一种垃圾回收算法,它给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1。任何时刻只要计数器为0则回收然而,这种算法无法解决对象之间互相循环引用的问题
例如,如果对象A引用对象B,而对象B又引用对象A,那么这两个对象的计数器永远不为0,即使这两个对象再也没有任何其他引用。在这种情况下,垃圾回收器无法回收这两个对象,这就是所谓的循环引用问题
垃圾回收算法
标记-清除(Mark-Sweep)
标记:找出内存中需要回收的对象,并且把它们标记出来
清除:清除掉被标记需要回收的对象,释放出对应的内存空间
缺点:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无 法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制(Copying)
将内存划分为两块相等的区域,每次只使用其中一块,如下图所示
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次 清除掉
缺点:空间利用率降低。
标记-整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活
的对象都向一端移动,然后直接清理掉端边界以外的内存,让所有存活的对象都向一端移动,清理掉边界意外的内存。
小结
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)
复制:发生在新生代(优点:无内存碎片,效率高 缺点:需要两倍的空间)
标记清理:发生在老年代(优点:占用空间少 缺点:会产生内存碎片)
标记整理:发生在老年代(优点:占用空间少,无碎片 缺点:对象移动,会消耗资源)
垃圾回收器的种类
1、Serial:串行(-XX:+UseSerialGC)
为单线程环境设计,且使用一个线程回收垃圾,会暂停所有的用户线程,不适合服务器环境(例如:用户用餐,餐厅叫出去要叫一个清洁工打扫,打扫完再回来吃)
2、Parallel:并行(-XX:+UseParallelGC)
多个并行垃圾收集线程工作,此时用户线程是暂停的,适用于科学计算、大数据处理首台处理等若交互环境(例如:用户用餐,餐厅叫出去要叫多个清洁工打扫,打扫完再回来吃)
3、CMS:(-XX:UseConcMarkSweepGC)
CMS(Concurrent Mark Sweep)垃圾回收器是第一个关注 GC 停顿时间的垃圾收集器1。CMS垃圾回收器的运行过程分为4个步骤1234:
初始标记:这个阶段需要「Stop the World」。初始标记其实就是对被我们GC ROOT直接引用的对象做一个标记2。这个步骤仅仅只是标记一下 GC Roots 能直接关联到的对象,并不需要做整个引用的扫描,因此速度很快1。
并发标记:在进行并发标记的过程中,我们的用户线程和CMS线程会一起执行。CMS所做的一件事情就是把堆里的所有引用对象全部找到并做标记2。这个阶段不需要「Stop the World」1。对整个引用链做扫描需要花费非常多的时间,因此通过垃圾回收线程与用户线程并发执行,可以降低垃圾回收的时间,从而降低系统响应时间1。
重新标记:在这一步,CMS会触发STW机制,并修复并发标记状态已经改变的对象,但是这个过程会比较漫长2。他利用三色标记和增量更新来解决我们的漏标问题2。这个阶段需要「Stop the World」1。
并发清理:那这一步就很好理解了,所谓的并发清理其实就是对没有被做标记的对象进行一个清理回收,在这个过程中同样不会产生STW2。这个阶段,垃圾回收线程与用户线程可以并发执行,因此并不影响用户的响应时间1。
这就是CMS垃圾回收器的工作过程。希望这个解释对你有所帮助!
4、G1:(garbage first)(-XX:UseG1GC)
G1垃圾回收器将堆内存分割成不通的区域然后并发的对其进行垃圾回收 java11默认GC回收>> 器是ZGC。属于标记-整理算法使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合
(1) 初始标记(Initial Marking) 标记一下GC Roots能够关联的对象,并且修改TAMS的值,需要暂 停用户线程
(2)并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发 执行
(3)最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需 暂停用户线程
(4)筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据 用户所期望的GC停顿时间制定回收计划
5、ZGC(Z Garbage Collector)
Z Garbage Collector (ZGC) 是一种可扩展的低延迟垃圾收集器。ZGC 执行所有昂贵的工作并发地进行,不会停止应用程序线程的执行超过几毫秒。它适用于需要低延迟的应用程序。暂停时间与正在使用的堆大小无关。
ZGC的工作原理主要包括以下几个步骤:
并发标记:ZGC首先并发地在堆中标记活动对象。这个阶段,ZGC遍历对象图以标记对象为活动或垃圾。这个阶段还包括重新映射活动数据。
并发预处理:这是进行引用预处理的地方。在这个阶段,ZGC处理软引用、弱引用、虚引用和JNI引用。
并发压缩:这是对堆进行紧凑的重要工作。在这个阶段,ZGC会遍历堆中的所有活动对象,并将它们移动到新的位置,以便将未使用的内存空间紧凑起来。在对象被移动后,原来的内存空间就被视为垃圾,并被回收。这个过程是并发的,也就是说,它在应用程序线程运行的同时进行,不会导致应用程序的停顿。
ZGC使用了一种称为负载屏障和彩色指针的技术来实现并发标记和压缩。负载屏障的目标是在指针加载时插入一个处理逻辑。如果指针指向一个已经被移动的对象,负载屏障将纠正指针。在标记阶段,如果指针没有被标记,负载屏障将标记指针。在压缩阶段,如果指针指向将要被移动的对象的区域,指针指向的对象将被移动,然后纠正指针。负载屏障确保每次加载指针时都可以访问正确的对象,当GC线程和Java线程并发运行时。
彩色指针的ZGC使用指针的未使用的上位位作为指针的颜色,以指示指针的状态。因此,当负载屏障处理指针时,负载屏障可以直接获取指针的状态,并决定如何处理指针。
ZGC的应用场景主要包括:
低延迟:Java ZGC能够最小化GC暂停并保持它们短暂。目标是暂停不超过10ms。
可扩展性:Java ZGC可以有效地处理多达几个太字节大小的堆。
易于使用:Java ZGC易于使用并且需要最少的配置。
大型应用程序:对于需要大量内存的大型应用程序,例如,为数千甚至数百万的用户提供服务的应用程序。
高吞吐量:尽管ZGC专注于最小化GC暂停时间,但它仍然提供了高吞吐量。
实时应用程序:对于需要高度响应的应用程序,例如游戏或实时应用程序,ZGC是理想的选择。
设置垃圾回收器
(1)串行 -XX:+UseSerialGC -XX:+UseSerialOldGC
(2)并行(吞吐量优先): -XX:+UseParallelGC -XX:+UseParallelOldGC
(3)并发收集器(响应时间优先) -XX:+UseConcMarkSweepGC -XX:+UseG1GC
举例
java -server -Xms1024m -Xmx1024m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal -XX:+UseG1GC -jar springboot2019-SNAPSHOT.jar
GC的类型
老年代采取的垃圾回收算法是标记整理算法 老年代触发垃圾回收的机制,一般就是两个;
- 在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下, 此时需要提前触发FullGC再然后再带着进行Minor GC;
- 在Minor GC之后,发现剩余对象太多放入老年代都放不下了
垃圾进入老年代的触发条件
1、当对象的年龄达到15岁时
默认的设置下,也就是躲过15次GC的时候,他就会转移到老年代里去 具体是多少岁进入老年代,可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
2、动态对象年龄判断
假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,
那么此时大于等于这批对象年龄的最大值对象,就可以直接进入老年代了
例如:年龄1+年龄2+年龄n,的多个年龄对象总和超过了Survivor区的50%,此时就会把年龄n以上的对象都放入老年代
3.大对象直接进入老年代
如果你要创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放到老年代里去,压根不会经过年轻代,有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把它的值设置为字节数,比如“1048576”字节,就是1MB
4.Minor GC后的对象太多
Minor GC后的对象太多无法放入Survivor区 这个时候就必须得把这些对象直接转移到老年代去