文章目录
- 垃圾回收(GC)
- 什么内存需要回收(什么样的对象是垃圾)?为什么要进行垃圾回收?
- 内存溢出和内存泄露的区别,如何解决
- 分区收集思想 Minor GC、Major GC、Full GC
- 垃圾回收相关算法
- 引用计数算法(在现代的JVM中并没有被使用)
- 可达性分析算法/根搜索算法
- 对象的 finalization 机制
- 垃圾回收阶段的算法
- 垃圾回收器
- 垃圾回收器分类
- CMS垃圾回收器(Concurrent Mark Sweep 并发标记清除)
- G1(Garbage-First)垃圾回收器
垃圾回收(GC)
无任何说明都是针对HotSpot虚拟机
好处:解放程序员,对内存管理更合理,自动化
不好的:对程序员管理内存的能力降低了, 解决问题能力变弱了, 不能调整垃圾回收的机制
对于Java程序员来说,在虚拟机内存管理机制下,不需要像C/C++语言那样手动去垃圾回收操作,不容易出现内存泄漏和内存溢出等问题。但正是因为Java程序员吧内存控制权交给Java虚拟机,一旦出现内存方面泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会非常艰难。
第一个使用自动垃圾回收的不是Java语言,
Java堆是垃圾收集器管理的主要区域,因此也成为GC堆(Garbage Collected Heap),从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆被划分了几个不同区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。
堆空间详解以及垃圾回收可以看—>Java内存区域详解
什么内存需要回收(什么样的对象是垃圾)?为什么要进行垃圾回收?
Java中的垃圾对象是指没有被任何引用变量所引用的对象。这些对象无法被访问,也无法被使用,因此它们占用内存空间而不被程序所使用,成为垃圾对象。如果不进行垃圾回收,内存迟早都会被消耗完,不断的分配内存空间而不进行回收,就像不停的生产生活垃圾而从来不进行打扫一样,垃圾回收也可以清除内存里的记录碎片,将这些碎片进行整理占用的内存移动到堆的一端,方便JVM将整理出的内存分配给新的对象(数组必须是连续空间)。
内存溢出和内存泄露的区别,如何解决
内存溢出指的是程序在申请内存时,由于没有足够的内存可用,而导致程序崩溃或者出现其他异常情况的现象。这通常是因为程序错误地使用了内存,例如未及时释放不需要的内存或者使用了太多内存资源,导致系统无法提供足够的内存来满足应用程序的需求。
内存泄漏指的是程序中存在一些对象或变量没有被垃圾回收器及时回收,导致这些对象一直占用着内存空间并最终耗尽可用内存的现象。通常是因为程序中存在不合理的设计或编码问题,例如忘记释放动态分配的内存、使用循环引用等等。还有就是打开了使用对象的东西,但是没有关闭,导致垃圾处理时认为对象处于运行状态,不会被回收处理,
IO
流close
和jdbc
链接close
没有关闭。
两者区别在于,内存泄漏是程序代码中存在的开发问题,内存溢出则是由于系统资源有限造成的结果。需要解决内存泄漏问题,通常需要审查代码并进行调试,而需要解决内存溢出问题,则需要考虑优化应用程序,增加可用内存资源,并可能需要进行代码重新设计,以便更有效地使用和释放内存。
分区收集思想 Minor GC、Major GC、Full GC
部分收集、整堆收集
- 新生区收集(Minor GC/Yang GC): 只是新生区(Eden ,S0,S1)的垃圾收集 ,回收比较频繁
- 老年区收集(Major GC/Old GC):只是对老年区的垃圾收集,回收的次数较少
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集,尽量避免
垃圾回收相关算法
标记阶段
作用:判断对象是否是垃圾对象,是否有引用指向对象
相关的标记算法:引用计数算法和可达性分析算法
引用计数算法(在现代的JVM中并没有被使用)
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就 +1;当引用失效时,计数器值就 -1 ;任何时刻计数器为 0 的对象就是不可能再被使用的。
//有个计数器来记录对象的引用数量
String s1 = new String("aaa");
String s2 = s1; //有两个引用变量指向aaa对象
s2 = null; -1
s1 = null; -1
引用计数法原理简单,效率也很高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,主要原因是引用计数就很难解决对象之间相互循环引用的问题。
例如,存在两个对象
objA
和objB
,他们都有字段instance
,令objA.instance=objB;objB.instance=objA
除此之外,这两个对象再无任何引用,实际上这两个对象已 经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
缺点:
需要维护计数器,占用空间,频繁操作需要事件开销
无法解决循环引用问题,多个对象之间相互引用,没有其他外部引用指向他们,计数器都不为0,不能回收,产生内存泄漏
可达性分析算法/根搜索算法
可达性分析算法是一种常用的内存管理算法,它的核心思想是从一组根对象(GCRoots)开始,通过遍历对象引用关系,确定哪些对象可以被访问到,哪些对象是不可访问的,然后把不可访问的对象标记为垃圾,最终回收这些不可访问的对象占用的内存空间。
可达性分析算法是当前主流的垃圾回收算法,由于它能够处理循环引用的情况,因此能够避免内存泄漏的问题,并且由于标记过程不需要对所有对象进行遍历,因此回收效率较高。
GC Roots 可以是哪些元素?
- 在虚拟机栈中被使用的.
- 在方法中存储的静态成员指向的对象
- 在方法区中常量引用的对象
- 作为同步锁使用的 synchronized
- 在虚拟机内部使用的对象
对象的 finalization 机制
对象的
finalization
机制是一种内存管理模式,它允许程序在对象被垃圾回收之前执行特定的清理和释放操作。在Java中,finalize()
方法是用于实现对象的finalization
机制的。
当一个对象变为垃圾之前,JVM会在内部自动调用其
finalize()
方法(如果该对象的finalize()
方法未被重写,则不会执行任何操作),并在finalize()
方法执行结束之后回收该对象。开发人员可以在finalize()
方法中编写释放资源、关闭打开的文件、清除临时数据等操作,以便程序尽快回收不再使用的内存空间。
需要注意的是,finalize() 方法调用具有不确定性,即无法保证 finalize() 方法执行的时间、频率和顺序。因此,不应该在 finalize() 方法中执行太复杂或太耗时的操作,否则可能会导致垃圾回收的效率下降或者程序出现异常情况。
有了
finalization
机制的存在,在虚拟机中把对象状态分为3种:
可触及的 ,不是垃圾,与根对象连接的
可复活的 ,判定为垃圾了,但是还没有调用
finalize()
,(在finalize()
中对象可能会复活)不可触及的: 判定为垃圾了,finalize()也被执行过了,这种就是必须被回收的对象
垃圾回收阶段的算法
标记-复制算法
标记复制算法是一种垃圾回收算法,它在内存分配时将堆内存分为两部分,每次只使用其中一块,当这一块满时,将其中的存活对象复制到另一块空闲的空间,然后把原来的空闲空间全部清除,切换使用这个空间,如此反复执行,达到回收垃圾的目的。
优点: 减少内存碎片
缺点: 如果内存中多数对象存活,则需要复制的对象数量多,效率低.
适用场景:存活对象少,新生代适合使用标记复制算法
标记-清除算法
标记清除算法是一种垃圾回收算法,它在进行内存回收时,会先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出所有仍然在使用中的对象,然后清除没有被标记的对象,释放它们所占用的内存空间。
清除不是真正的把垃圾对象清除掉,而是将垃圾对象地址维护到一个空闲列表中,后面有新对象到来时,覆盖掉垃圾对象即可。
优点: 标记清除算法具有简单、可行的特点,适用于长时间运行、堆内存分配较为稳定的场景
缺点:但由于清除过程需要扫描堆的所有内存块,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,因此效率较低,且可能会使剩余内存出现不连续的碎片化,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。在快速分配和释放内存的场景下,标记清除算法的效率会受到较大影响。
标记-压缩算法(标记-整理)
标记-整理算法:其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,一般用于对于长时间运行的老年代堆空间进行回收操作。
优点: 标记压缩算法相较于标记清除算法来说,能够减少内存碎片,简化内存回收操作
缺点: 其实现效率通常较低,因为它需要花费额外的时间和空间来进行压缩操作,并且移动对象的操作也会带来额外的复杂性和开销。移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。
总结
由以上三种算法可以看出:是否移动对象都存在弊端,移动的话内存回收更复杂,不移动则内存分配会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。
此外就出现了另一种解决方案:
- 可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间
垃圾回收器
垃圾回收算法是方法论,垃圾回收器是内存回收的具体实现
虽然我们对各个收集器进行比较,但是并非要选择一个最好的收集器,因为直到现在还没有最好的垃圾收集器出现,更没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器
垃圾回收器分类
-
按照线程数量分
单线程垃圾回收器:Serial Serial old
只有一个线程进行垃圾回收,适用于小型简单的适用场景,垃圾回收时,其他用户线程会暂停
多线程垃圾回收器:Parallel
多线程垃圾回收器内部提供多个线程进行垃圾回收,在多cpu情况下大大提升垃圾回收效率,但是同样也会暂停其他用户线程。
-
按照工作模式分为
独占式:垃圾回收线程执行时,其他线程暂停
并行式:垃圾回收线程可以和用户线程同时执行
-
按工作的内存区间
年轻代垃圾回收器,老年代垃圾回收器
垃圾回收器(GC)性能指标
暂停时间(在垃圾回收过程中,其他线程暂停)
吞吐量:运行用户代码的时间占总运行时间的比例
垃圾收集开销: 垃圾收集所用时间与总运行时间的比例
暂停时间: 执行垃圾收集时,程序的工作线程被暂停的时间
内存占用:Java堆区域所占的内存大小
快速: 一个对象从诞生到被回收所经历的时间
CMS垃圾回收器(Concurrent Mark Sweep 并发标记清除)
CMS垃圾回收器是一种面向服务器端应用程序的内存回收器。相较于其他垃圾回收器,CMS垃圾回收器采用并发的方式进行垃圾回收,以减少应用程序的暂停时间,提高程序的可用性和性能。 与其他垃圾回收器不同的是,它的垃圾回收过程并不是在暂停应用程序的情况下完成的,而是并发进行的,因此不会对应用程序的性能产生明显的影响。
CMS收集器是一种标记-清除算法,相较于前面几种垃圾收集器来说更为复杂,整个过程四个步骤
初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快
并发标记:同时开启GC和用户线程,用一个闭包结构与记录可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
重新标记: 修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,暂停所有的其他线程,停顿时间比初始标记稍微长,但远远比并发标记阶段时间短。
并发清除:垃圾回收线程与用户线程并发(同时)执行 ,对为标记的区域进行垃圾对象的清除
优点:可以作到并发收集
弊端:使用标记清除算法,会产生内存碎片,并发执行影响到用户线程,无法处理浮动垃圾
三色标记:
由于CMS有并发执行过程,所以在标记垃圾对象时有不确定性。
所以在标记时,将对象分为三种颜色(3中状态)
黑色: 例如GCRoots确定是存活的对象
灰色:在黑色对象中关联的对象,其中还有未扫描完的,之后还需要再次进行扫描
白色:与黑色,灰色对象无关联的,垃圾收集算法不可达的对象
标记过程:
1.先确立GCRoots,把GCRoots标记为黑色
2.与GCRoots关联的对象标记为灰色
3.再次遍历灰色,灰色变为黑色,灰色下面有关联的对象,关联的对象变为灰色
4.最终保留黑色,灰色,回收白色对象
可能会出现漏标,错标问题
G1(Garbage-First)垃圾回收器
G1是Java虚拟机中的一种新型、高效的垃圾回收器,它在JDK7中首次推出,是目前Java虚拟机中最受欢迎的垃圾回收器之一。
相比于传统的垃圾回收器,G1具有以下优点:
- 高效、可预测的垃圾回收:G1在堆内存分为多个区域,每个区域都会被扫描,并针对性的执行清理操作。因此,G1能够同时实现高效和可预测的垃圾回收。
- 系统吞吐量更高:相比于传统的垃圾回收器,G1在长时间的运行中系统吞吐量表现更加稳定,同时能达到更高的吞吐量。
- 更好的平均响应时间:G1在执行垃圾回收时不会导致长时间的停顿,这样可以获得更好的平均响应时间。
- 更少的内存消耗:G1将整个堆内存划分为多个区域,垃圾回收时只需要扫描需要回收的区域,因此可以减少不必要的内存消耗。
但是,与其它垃圾回收器相比,G1也有一些不足之处。比如G1需要更多的CPU和内存资源,可能会对应用程序的性能产生一定的影响。此外,在堆内存比较小的情况下,G1的表现也不够理想。