概叙
科普文:一文搞懂jvm(一)jvm概叙-CSDN博客
科普文:一文搞懂jvm原理(二)类加载器-CSDN博客
科普文:一文搞懂jvm原理(三)执行引擎-CSDN博客
科普文:一文搞懂jvm原理(四)运行时数据区-CSDN博客
前面我们介绍了jvm,jvm主要包括两个子系统和两个组件: Class loader(类装载器) 子系统,Execution engine(执行引擎) 子系统;Runtime data area (运行时数据区域)组件, Native interface(本地接口)组件。
由于篇幅限制,前面讲解执行引擎时,垃圾回收器跳过了,这里就详细讲解 ,毕竟和jvm优化相关。
图二:这个是前面文中多次出现的jvm详细图,
jvm作用:
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里
jvm特点:自动内存管理(内存分配和回收)
跨平台:一次编译,到处运行;这是JVM 的主要特征之一,Java 程序在编译为字节码后可以在任何支持 JVM 的平台上运行,摆脱了硬件平台的束缚,实现了"一次编译,到处运行"的理想。
自动内存管理(内存分配和回收):JVM 提供了自动的内存管理机制,包括内存分配、垃圾回收和内存优化。开发者无需手动分配和释放内存,JVM 会自动管理对象的生命周期和内存回收,通过垃圾回收器(Garbage Collector)自动回收不再使用的对象,避免了内存泄漏和悬挂指针等问题。
即时编译:JVM 通过即时编译器将热点代码动态编译成本地机器码,提高程序的执行性能。编译器可以根据程序的运行情况进行优化,使得Java应用能随着运行事件的增长而获得更高的性能。
一、什么是垃圾?
在了解垃圾回收机制之前我们首先要定义一下什么是垃圾?
垃圾:简单说就是内存中已经不在被使用到的内存空间就是垃圾。
我们内存里大部分的对象都是随着方法的执行而创建,方法执行完毕后这些对象就不会被再次使用了,而这些不会被再次使用的对象并不会被清除掉,所以我们内存里面的对象会越来越多占用着我们的内存空间,此时我们就需要一种机制把这种不会被再次使用的对象清除掉,而这些不会被再次使用的对象我们就称之为垃圾。
二、JVM垃圾的危害?垃圾回收
垃圾回收(Garbage Collection,GC):顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放空间,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?
1960年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时 Java 还没有出世呢!所以实际上 GC 并不是Java的专利,GC 的历史远远大于 Java 的历史!
内存泄露(memory leak)
程序运行结束后,没有释放 所占用的内存空间。
一次内存泄漏 似乎不会有大的影响,但内存泄漏 不断累积,最终可用内存会变得越来越少。
比如说,总内存大小是100 MB,有40MB的内存一直无法回收,那么可用的只有60MB 。这40MB的就是内存泄漏。
内存泄漏,就是程序运行结束后,没有释放的内存。
内存溢出(out of memory)
程序运行时,在申请内存空间时,没有足够的内存空间供其正常使用,程序运行停止,并抛出 out of memory 。
比如程序运行时申请了一个10MB 空间, 但是当前可用内存只有5MB,程序无法正常执行,这就是内存溢出。
内存溢出 ,可以理解为程序运行需要的内存大于当前可用的内存。
二者的区别和联系
区别
内存泄露: 程序运行结束后,所占用的内存没有全部释放。
内存溢出:程序运行时,需要的内存大于当前可用的内存,内存不足,程序无法继续执行,抛出 “内存溢出”,程序运行中断,结束。
联系
一次 内存泄露 可能对程序运行没有明显的影响,多次 内存泄露 最终会导致 内存溢出 。
比如总内存大小是100MB,一次程序运行结束有,有10MB 没有释放,当前可用内存还有90MB,程序还可以运行。但是多次运行后, 可用内存 最终为0, 没有可以内存或内存不足时,程序在下一次运行时,会因为内存不足,而出现 内存溢出 。
内存溢出的原因
引起内存溢出的原因有很多种,小编列举一下常见的有以下几种:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;启动参数内存值设定的过小 。
内存溢出的解决方案
第一步,修改JVM启动参数,直接增加内存。(-Xms、-Xmx 参数一定不要忘记加)
第二步,检查错误日志,查看 “OutOfMemory” 错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。
-
- 一般来说,如果一次取十万条记录到内存,就可能引起内存溢出(大文件导入)。
- 这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,
- 数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
4.检查对数据库查询中,是否有一次获得全部数据的查询。
- 一般来说,如果一次取十万条记录到内存,就可能引起内存溢出(select * from 大表 不加分页或者游标)。
- 这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,
- 上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
5.检查List、MAP等集合对象是否有使用完后,未清除的问题。
List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况。
三、如何判断对象是否可以被回收?
判断对象是否被回收其实就是判断对象是否会被再次使用,常用的算法有引用计数法和可达性分析法。
1、垃圾回收机制?
答:在学习Java GC 之前,我们需要记住一个单词:stop-the-world(STW) 。它会在任何一种GC 算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行。当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC 优化很多时候就是减少 stop-the-world 的发生或者说是减少 stop-the-world 的时间。
2、GC回收哪个区域的垃圾?
答:JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。
3、GC怎么判断对象可以被回收?
答:当某个对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
- 对象没有引用;
- 作用域发生未捕获异常;
- 程序在作用域正常执行完毕;
- 程序执行了System.exit();
- 程序发生意外终止(被杀线程等)。
4、Java 中都有哪些引用类型?
- 强引用: 发生 GC 的时候不会被回收。
- 软引用: 有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用: 有用但不是必须的对象,在下一次GC时会被回收。
- 虚引用(幽灵引用/幻影引用): 无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 GC 时返回一个通知。
引用分类
StrongReference: 强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
public class Main { public static void main(String[] args) { new Main().fun1(); } public void fun1() { Object object = new Object(); Object[] objArr = new Object[1000]; } }
SoftReference: 软引用
软引用是用来描述一些有用但并不是必需的对象,在 Java 中用 java.lang.ref.SoftReference
类来表示。对于软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。因此,这一点可以很好地用来解决 OOM 的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:
import java.lang.ref.SoftReference; public class Main { public static void main(String[] args) { SoftReference<String> sr = new SoftReference<String>(new String("hello")); System.out.println(sr.get()); } }
WeakReference: 弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { WeakReference<String> sr = new WeakReference<String>(new String()); System.out.println(sr.get()); System.gc(); //通知JVM的gc进行垃圾回收 System.out.println(sr.get()); } }
PhantomReference: 虚引用
“虚引用”也称为幽灵引用或幻影引用,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在 Java 中用 java.lang.ref.PhantomReference
类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class Main { public static void main(String[] args) { ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String(), queue); System.out.println(pr.get()); } }
跨代引用
-
跨代引用:也就是一个代中的对象引用另一个代中的对象。
-
跨代引用假说:跨代引用相对于同代引用来说只是极少数。
-
隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡。
四、 垃圾回收器(Garbage Collection,简称GC)
垃圾回收器(Garbage Collection,简称GC)。它负责跟踪哪些对象仍然在使用,哪些对象已经不再被引用,并释放那些不再被引用的对象所占用的内存空间。这一过程涉及到对象的标记、清除、压缩等多个阶段,每个阶段都有其特定的算法和策略。
随着Java技术的不断发展,JVM的垃圾回收机制也在不断地优化和完善。从早期的串行回收器到并行回收器,再到现代的CMS(Concurrent Mark-Sweep)回收器和G1(Garbage-First)回收器,每一次的演进都带来了性能上的提升和特性上的丰富。
然而,尽管JVM的垃圾回收机制已经相当成熟和高效,但在某些特定的场景下,我们仍然需要对其进行调优和优化,以满足特定的性能需求。这要求我们深入理解JVM的垃圾回收机制,熟悉各种回收器的特点和使用场景,并能够根据实际情况选择合适的回收器和配置参数。
JVM中垃圾的定义
Java 内存运行时区域中的程序计数器、虚拟机栈、本地方法栈随线程而生灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由 JIT 编译器进行一些优化),因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而 Java 堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
在JVM中,如果堆中的对象不再被任何变量引用被视为垃圾,我们通常通过:User u = new User() 来创建对象,不考虑栈中分配对象(逃逸分析)的情况变量u在栈中,User实例在堆中分配内存,随着线程执行,方法结束,栈帧销毁u也会跟着释放,那么堆中的User对象就变成了无引用的对象,也就是垃圾对象,等待被垃圾回收器回收。
要回收垃圾就必须要标记哪些对象是垃圾对象
,就有了垃圾标记算法,JVM的垃圾标记算法主要用于确定哪些对象在内存中不再被引用,从而可以被垃圾回收器回收。
判断对象是否被回收其实就是判断对象是否会被再次使用,常用的算法有引用计数法和可达性分析法。
垃圾判定算法(垃圾标记算法)--引用计数法(Reachability Counting)
1、无法解决循环引用的问题
引用计数法虽然很直观高效,但是通过引用计数法是没办法扫描到一种特殊情况下的“可回收”对象,这种特殊情况就是对象循环引用的时候,比如A对象引用了B,B对象引用了A,除此之外他们两个没有被任何其他对象引用,那么其实这部分对象也属于“可回收”的对象,但是通过引用计数法是没办法定位的。
2、另外一个方面是引用计数法需要额外的空间记录每个对象的被引用的次数,这个引用数也需要去额外的维护。
垃圾判定算法(垃圾标记算法)--可达性分析法(Reachability Analysis)
哪些对象对象我们称之为"GC Roots"对象呢? 当然普通的对象肯定是不行的,如果要作为GC Roots 对象那么它自身肯定得满足一个条件,那就是他自己一定在很长一段时间内都不会被GC 回收掉。
那么只有满足这个条件的对象才可能作为GC Roots了,GC Roots的类型大致如下:
1、虚拟机栈中的本地变量所引用的对象。
2、方法区中静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法中(Native方法)引用的对象。
5、虚拟机内部的引用对象(类记载器、基本数据对应的Class对象,异常对象)。
6、所有被同步锁(Synchronnized)持有的对象。
7、描述虚拟机内部情况的对象(如 JMXBean、JVMTI中注册的回调、本地缓存代码)。
8、垃圾搜集器所引用的对象。
finalize方法
当对象被标记为垃圾意味着被判了死刑,但是其实他们并不是必死无疑,还有挽救的余地。进行可达性分析后对象和GC Roots之间没有引用链相连时,对象将会被进行一次标记,接着会判断如果对象没有覆盖Object的finalize()方法或者finalize()方法已经被虚拟机调用过,那么它们就会被行刑(清除);
如果对象覆盖了finalize()方法且还没有被调用,则会执行finalize()方法中的内容,所以在finalize()方法中如果重新与GC Roots引用链上的对象关联就可以拯救自己,但是一般不建议这么做.
总结来说,JVM主要使用可达性分析算法来标记垃圾对象,而引用计数算法由于其无法解决循环引用的问题,在JVM中并不常用。在垃圾回收过程中,JVM会结合具体的垃圾回收算法(如标记-清除、标记-整理、复制等)来高效地回收不再被引用的对象所占用的内存空间。
JVM中的垃圾回收算法
当我们通过可达性分析法来定位对象是否存活后,我们就需要通过某种策略把这些已死的对象进行清理、然后对存活的对象进行整理,这个过程就涉及到三种算法,分别为标记清除法、标记复制法、标记整理法;分代回收收集理论暂不作为一种算法,只是标记复制和标记清除的一种组合;三色算法(CMS和G1)。
1.分代回收理论
根据对象的存活周期长短不同,将内存划分为新生代和老年代,存活周期短的为新生代,存活周期长的为老年代。这样就可以根据每块内存的特点采用最适当的收集算法。
新生代的中每次垃圾收集中会发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中因为对象的存活率高,没有额外的控件对它进行分配担保,就必须使用“标记-清扫”或者“标记-整理”算法来进行回收。
2、标记复制算法
将内存分为大小相同的2块,每次只使用其中一块,另一块内存预留。当一块内存使用完后,将存活的对象复制到另一块预留内存里,再清空之前那块内存。
按照容量划分两个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
- 优点:清除垃圾效率高,直接将整片空间清除;没有内存碎片
- 缺点:内存利用率不高(因为总有一块内存是不用的,用来复制的时候做中间缓冲区的),只有原来的一半。
- 典型应用:年轻代的GC,存活对象从eden区、survivor-from到survivor-to的时候
3、标记-清除算法
标记清除是2个阶段,即“标记”和“清除”标记存活的对象,清除未标记的对象;也可以反过来,找出要回收的对象进行标记,然后统一回收。推荐使用前者。
这种简单的标记清除算法会带来2个问题,效率问题和空间问题,
其一如果需要标记的对象太多,导致效率不高。
其二标记清除后产生大量不连续的碎片内存。
标记-清除算法: 标记无用对象,然后进行清除回收。
- 优点:内存利用率高,没有闲置内存
- 缺点:效率低,特别是需要标记的对象比较多的时候;会产生内存碎片,由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理效率不高,无法清除垃圾碎片。
- 典型应用:老年代的GC(部分垃圾收集器)
4、标记-整理算法
根据老年代特出的一种算法,标记过程与标记清除算法一样,不一样的是回收过程是将存活的对象向一端挪动,然后在清理掉边界以外的内存。
标记-整理算法: 标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
- 优点:内存利用率高,没有闲置内存,没有内存碎片
- 缺点:效率低,成本更高,特别是需要标记的对象比较多的时候
- 典型应用:老年代的GC(部分垃圾收集器)
5、三色算法
public class ThreeColorRemark {
public static void main_1(String[] args) {
A a = new A();
B b = new B();
// 假设在即将开始“重新标记”之前,代码执行到了这里
a.c.d = b.d;
b.d = null;
}
public static void main_2(String[] args) {
A a = new A();
B b = new B();
// 假设在即将开始“并发标记”之前,代码执行到了这里
a.c.d = b.d;
b.d = null;
// 即将进入“重新标记”
...
...
...
}
class A {
C c = new C();
}
class B {
C c = new C();
D d = new D();
}
class C {
D d = null;
}
class D {
}
}
下面这张图,是对应的main_1方法的GC过程(注意main_1的条件,只是假设即将开始【重新标记】之前,代码也开始运行到a.c.d = b.d;了 )
接下来放第二张图,对应的main_2方法的GC过程(注意main_2的条件,只是假设即将开始【并发标记】之前,代码也开始运行到a.c.d = b.d;了)
看到了没,由于代码在进入“并发标记”之前,已经扫描过C了,并且确认了C并无引用,所以置黑了,到这里没毛病。但是后面没想到B切断了对D的引用,继续遍历B的话,会误以为没有其他可引用对象,接着就置黑了B,但是我们上帝视角看得很清楚,D还没有被扫描啊,完犊子了…(这里就是传说中的【漏标】)
JVM中的垃圾回收器
下图提供10种垃圾收收集器,重点讲解CMS、G1垃圾收集器。在JDK8版本中默认是Parallel GC(并行垃圾收集器)。此外,JDK1.8中还提供了其他几种垃圾收集器,如CMS(Concurrent Mark Sweep)收集器、G1(Garbage First)收集器;而JDK11引入的ZGC:Z Garbage Collector 垃圾收回器,都已经没有分代的概念,ZGC最终在 JDK 15中被宣布为 Production Ready。
我们可以不同的场景通过设置JVM参数来选择使用不同的垃圾收集器。
- Serial (复制算法):最早的单线程串行垃圾回收器。
- ParNew (复制算法):是 Serial 的多线程版本。
- Parallel Scavenge (复制算法):Parallel 和 ParNew收集器类似是多线程的,但 Parallel Scavenge 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
- Serial Old (标记-整理法):Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
- Parallel Old (标记整理法):Parallel Old 是 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法。
- CMS (标记-整理法):一种以牺牲吞吐量为代价来获得最短回收停顿时间为目标的收集器,非常适用 B/S 系统。
- G1 (标记-整理法 + 复制算法):一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK9 以后的默认 GC 选项。
- ZGC(标记 - 整理算法):ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC。
垃圾收集器分类
GC 性能指标:
吞吐量:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) 程序的运行时间/程序的运行时间 + 内存回收的时间
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
收集频率:相对于应用程序的执行,收集操作发生的频率
内存占用:Java 堆区所占的内存大小
快速:一个对象从诞生到被回收所经历的时间
垃圾回收器组合关系
垃圾收集器虽然看起来数量比较多,但其实总体逻辑都是因为我们硬件环境的升级而演化出来的产品,不同垃圾收集器的产生总体可以划分为几个阶段。。
第一阶段:单线程收集时代(Serial和Serial Old)
第二阶段:多线程收集时代(Parallel Scanvenge 和Parallel Old)
第三阶段:并发收集时代(ParNew和CMS)
第四阶段:智能并发收集时代(G1、ZGC)
- Serial (复制算法):最早的单线程串行垃圾回收器。
- ParNew (复制算法):是 Serial 的多线程版本。
- Parallel Scavenge (复制算法):Parallel 和 ParNew收集器类似是多线程的,但 Parallel Scavenge 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
- Serial Old (标记-整理法):Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
- Parallel Old (标记整理法):Parallel Old 是 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法。
- CMS (标记-整理法):一种以牺牲吞吐量为代价来获得最短回收停顿时间为目标的收集器,非常适用 B/S 系统。
- G1 (标记-整理法 + 复制算法):一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK9 以后的默认 GC 选项。
- ZGC(标记 - 整理算法):ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC。
Serial 串行收集器(标记复制+标记整理算法):最早的单线程串行垃圾回收器
- Serial:适用于年轻代,使用复制算法
- Serial Old:适用于老年代,使用标记整理算法。它可以与其他多种垃圾回收器一起组合使用
从上图,我们可以看出来,这个垃圾收集器优缺点都很明显。比如:
- 优点:简单,垃圾回收也还算高效(相对部分垃圾收集器来说)
- 缺点:单线程,垃圾回收相对多线程比较慢。单核下效率高,但是多核情况下就有点浪费CPU了。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器。主要有两大用途:一种用途是在小于等于JDK1.5 版本中与Parallel Scavenge收集器搭配使用,另一种可以作为CMS收集器的后备方案。
ParNew (标记复制+标记整理):是 Serial 的多线程版本
Parallel Scavenge 并行清除收集器(标记复制+标记整理)
- Parallel Scavenge:适用于年轻代,使用复制算法
- Parallel Old:适用于老年代,使用标记整理算法
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的情况下可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集 器)。
CMS回收器:Concurrent Mark Sweep
CMS:Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现。JDK1.7之前的默认垃圾回收算法,并发收集,停顿小。
过程:
1、初始标记:stop-the-world,标记GCRoots直接关联的对象
2、并发标记:和用户线程并行执行,标记所有可达对象
3、重新标记:stop-the-world,对并发标记阶段用户线程运行产生的垃圾对象进行标记修正
4、并发清理:清理垃圾对象,和用户线程同时执行
5、并发重置:重置CMS收集器的数据结构
优点:
并发,低停顿
缺点:
1、对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢
2、无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾
3、CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了
G1回收器:Garbage First
G1:Garbage First,是一款面向服务端应用的垃圾收集器。
特点:
1、G1垃圾收集器将整个 JVM 内存分为多个大小相等的region,年轻代和老年代逻辑分区 。
2、G1 是 Java9 以后的默认垃圾回收器
3、G1 在整体上使用标记整理算法,局部使用复制算法
4、G1 的每个 Region 大小在 1-32M 之间,可以通过-XX:G1HeapRegionSize=n 指定区大小。
5、总的 Region 个数最大可以存在 2048 个,即heap最大能够达到32M*2048=64G
6、0.5<obj<1,那么放到old区,old标记为H 1<obj<n,连续的n个region,作为H
过程:
初始标记:标记出GCRoot对象,以及GCRoot所在的Region(RootRegion)
Root Region Scanning:扫表整个old的Region
并发标记:并发追溯标记,进行GCRootsTracing的过程
最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
清理回收:根据时间来进行价值最大化的回收,重置rset
ZGC回收器:Z Garbage Collector
ZGC:Z Garbage Collector 垃圾收回器,是一种可伸缩的低延迟垃圾收集器。
参考:
https://zhuanlan.zhihu.com/p/585254683
JVM工作原理与实战(四十):ZGC原理-CSDN博客
【JVM专题】垃圾收集器Serial&Parallel&ParNew&CMS&G1&ZGC与底层三色标记算法详解_jdk垃圾回收器 parallel g1 zgc-CSDN博客
逻辑上一次ZGC分为Mark(标记)、Relocate(迁移)、Remap(重映射)三个阶段
- Mark: 所有活的对象都被记录在对应Page的Livemap(活对象表,bitmap实现)中,以及对象的Reference(引用)都改成已标记(Marked0或Marked1)状态
- Relocate: 根据页面中活对象占用的大小选出的一组Page,将其中的活对象都复制到新的Page,并在额外的forward table(转移表)中记录对象原地址和新地址对应关系
- Remap: 所有Relocated的活对象的引用都重新指向了新的正确的地址
实现上,由于想要将所有引用都修正过来需要跟Mark阶段一样遍历整个对象图,所以这次的Remap会与下一次的Remark阶段合并。所以在GC的实现上是2个阶段,即Mark&Remap阶段和Relocate阶段
使用场景对比:
G1适合8/16G以上的内存使用,原因在于G1 rescan更快,清除垃圾时虽然是stop the world但是可控, CMS虽然是并发但是不可控,大块内存要回收会影响到应用程序的性能。
另外由于G1在清理垃圾时使用STW,所以可以采用标记整理算法,没有内存碎片问题
ZGC作为下一代垃圾回收器,性能非常优秀。ZGC垃圾回收过程几乎全部是并发,实际STW停顿时间极短,不到10ms。这得益于其采用的着色指针和读屏障技术。
6、Minor GC、Major GC和Full GC 介绍
6.1、Minor GC 清理年轻代
答: Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作。当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发 stop-the-world,但是它的回收速度很快。
6.2、Major GC 清理老年代
答:Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次****Minor GC。
6.3、Full GC 清理整个堆空间—包括年轻代、老年代和元空间
答:Full GC是针对整个新生代、老生代、元空间(metaspace,Java8以上版本取代perm gen)的全局范围的 GC。Full GC不等于Major GC,也不等于Minor GC + Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
Major GC 通常是跟 full GC 是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人major GC的时候一定要问清楚他想要指的是上面的full GC还是老年代。
6.4、JVM GC 什么时候执行?
答:Minor GC,Full GC触发GC。
Minor GC :系统自动触发的机制只有一个,就是Eden 区没有足够的空间分配给新创建的对象。
Full GC :
- 老年代空间不足,这个很简单,就是字面上的不足,例如:大对象不停的直接进入老年代,最终造成空间不足;
- 方法区空间不足;
- Minor GC 引发 Full GC 这个才是本文想重点介绍的。
6.5、为什么 Minor GC 会引发 Full GC 呢?引发条件是什么?
答: 年轻代的对象在经历Minor GC 过后,部分对象存活对象或全部存活对象会进入老年代。新生代与老年代的比例的值为1:2 (该值可以通过参数 –XX:NewRatio 来指定)。
引发条件
- 老年代剩余连续内存空间 > 新生代对象总空间>历次晋升到老年代的对象的平均大小,万事大吉,Minor GC 直接运行;
- 新生代对象总空间>老年代剩余连续内存空间>历次晋升到老年代的对象的平均大小,这下又要分情况了,主要是看是否设置了 HandlePromotionFailure 参数,JDK1.6之后该参数废弃了,但是机制仍在。执行Minor GC;
- 第一种可能,Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可;
- 第二种可能,Minor GC过后,剩余的存活对象的大小,是大于Survivor区域的大小,但是是小于老年代可用内存大小 的,此时就直接进入老年代即可;
- 第三种可能,很不幸,Minor GC 过后,剩余的存活对象的大小,大于了 Survivor 区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触 发一次“Full GC”。 如果 Full GC 仍空间不够就会 OOM。
- 新生代对象总空间> 历次晋升到老年代的对象的平均大小>老年代剩余连续内存空间: 则不会触发Minor GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括新生代,所以不需要事先触发一次单独的Minor GC)
6.6、按代的垃圾回收机制
答:默认的新生代(Young generation):老年代(Old generation )所占空间比例为 1 : 2 。
- 新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称之为:Minor GC 。
- 老年代(Old generation):对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC 或者 Full GC 。
- 持久代(Permanent generation)也称之为 方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:
- 所有实例被回收
- 加载该类的ClassLoader 被回收
- Class对象无法通过任何途径访问(包括反射)
6.7、为什么老年代用标记整理算法,新生代用复制算法?
答: 新生代的对象往往都是“朝生暮死”,因此新生代的对象在标记之后存活的对象较少,可以通过复制来提高算法的效率。老年代一般不会发生GC,加之老年代GC的对象也较少,因此采用标记整理法进行清理,可以有效的提高效率。
6.8、如果老年代的对象需要引用新生代的对象,会发生什么呢?
答: 为了解决这个问题,老年代中存在一个 card table,它是一个512byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。这个card table 由一个 write barrier 来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但完全是值得的。
6.9、新生代空间的构成?
答: 为了更好的理解GC,我们来学习新生代的构成,它用来保存那些第一次被创建的对象,它被分成三个空间:
- · 一个伊甸园空间(Eden)
- · 两个幸存者空间(Fron Survivor、To Survivor)
默认新生代空间的分配:****Eden : Fron : To = 8 : 1 : 1
每个空间的执行顺序如下:
1、绝大多数刚刚被创建的对象会存放在伊甸园空间(Eden)。
2、在伊甸园空间执行第一次 GC ( Minor GC ) 之后,存活的对象被移动到其中一个幸存者空间(Survivor)。
3、此后,每次伊甸园空间执行GC后,存活的对象会被堆积在同一个幸存者空间。
4、当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。然后会清空已经饱和的哪个幸存者空间。
5、在以上步骤中重复N次( N = MaxTenuringThreshold (年龄阀值设定,默认15)) 依然存活的对象,就会被移动到老年代。
从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。如果两个两个幸存者空间都有数据,或两个空间都是空的,那一定是你的系统出现了某种错误。
我们需要重点记住的是,对象在刚刚被创建之后,是保存在伊甸园空间的(Eden)。那些长期存活的对象会经由幸存者空间(Survivor)转存到老年代空间(Old generation)。也有例外出现,对于一些比较大的对象(需要分配一块比较大的连续内存空间)则直接进入到老年代。一般在 Survivor 空间不足的情况下发生。
6.10、老年代空间的构成与逻辑
答:老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从 Survivor 空间中熬过来的,它们绝不会轻易的狗带。因此,Full GC(Major GC)发生的次数不会有Minor GC 那么频繁,并且做一次 Major GC 的时间比 Minor GC 要更长(约10倍)。