JVM系列整体栏目


内容链接地址
【一】初识虚拟机与java虚拟机https://blog.csdn.net/zhenghuishengq/article/details/129544460
【二】jvm的类加载子系统以及jclasslib的基本使用https://blog.csdn.net/zhenghuishengq/article/details/129610963
【三】运行时私有区域之虚拟机栈、程序计数器、本地方法栈https://blog.csdn.net/zhenghuishengq/article/details/129684076
【四】运行时数据区共享区域之堆、逃逸分析https://blog.csdn.net/zhenghuishengq/article/details/129796509
【五】运行时数据区共享区域之方法区、常量池https://blog.csdn.net/zhenghuishengq/article/details/129958466
【六】对象实例化、内存布局和访问定位https://blog.csdn.net/zhenghuishengq/article/details/130057210
【七】执行引擎,解释器、JIT即时编译器https://blog.csdn.net/zhenghuishengq/article/details/130088553
【八】精通String字符串底层机制https://blog.csdn.net/zhenghuishengq/article/details/130154453
【九】垃圾回收底层原理和算法以及JProfiler的基本使用https://blog.csdn.net/zhenghuishengq/article/details/130261481

垃圾回收篇底层原理及相关算法

  • 一,垃圾回收篇底层原理
    • 1,垃圾回收概述
      • 1.1,什么是垃圾
      • 1.2,为什么需要gc
      • 1.3,java垃圾回收机制
    • 2,垃圾回收算法
      • 2.1,垃圾标记阶段
        • 2.1.1,引用计数算法
        • 2.2.2,可达性分析算法
    • 3,JProfiler查看GC Root
    • 4,垃圾回收相关算法
      • 4.1,标记清除算法
      • 4.2,复制算法
      • 4.3,标记整理算法
      • 4.4,三种算法总结
    • 5,垃圾回收相关概念
      • 5.1,System.gc() 的理解
      • 5.2,内存溢出和内存泄漏
        • 5.2.1,内存溢出(OOM)
        • 5.2.2,内存泄漏
      • 5.3,Stop The World
      • 5.4,引用

一,垃圾回收篇底层原理

1,垃圾回收概述

1.1,什么是垃圾

垃圾收集,并不是Java语言的产物,早在1960年,第一门使用内存动态分配和垃圾收集技术的Lisp语言诞生。垃圾回收机制也是Java的招牌能力,极大地提高了开发效率。因此在面对垃圾回收时,需要解决三个主要的问题:哪些内存需要回收、什么时候回收、如何回收?

垃圾:指的是在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如创建了某个对象,引用该对象的变量是存储在虚拟机栈的栈帧中,随着入栈出栈该栈帧被销毁,那么栈帧中引用该对象的局部变量变量也被销毁,此时没有任何变量引用着刚刚创建的对象,那么该对象就会变成垃圾,等待回收。

1.2,为什么需要gc

首先如果垃圾不回收,内存很容易被消耗完,在面对一个大系统的时候,没有GC就很难保证应用程序正常运行。

如果不及时的对内存中的垃圾进行回收, 那么这些对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至会出现内存溢出的情况。

1.3,java垃圾回收机制

java内部通过自动内存管理的方式,无需开发人员手动参与内存的分配和回收,这样可以降低内存泄漏和内存溢出的问题,从而省去繁重的内存管理,可以更加专注的进行业务开发。

然而自动内存管理就如同一个黑匣子,如果过度依赖自动化,那么就会弱化开发人员在程序出现内存溢出时定位问题和解决问题的能力。因此当需要排查各种内存溢出、内存泄漏等问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对这个垃圾回收进行必要的监控和调节

在进行垃圾回收时,主要针对的是Java堆的这个区域。从次数上来说,频繁的收集Young区,较少的收集Old区域,基本不动方法区(永久代或者元空间)

2,垃圾回收算法

在这垃圾回收的算法中,主要是做了两件事情:一件是找出垃圾,另一件是清除垃圾

2.1,垃圾标记阶段

垃圾标记阶段主要就是找出垃圾,用于判断对象是否存活。堆中几乎所有的对象都存储在堆中,在GC执行垃圾回收之前,需要先区分哪些是存活的对象,哪些是已经死亡的对象。只有被标记已经死亡的对象,GC才会在执行垃圾回收时,才会释放掉其占用的内存,这个阶段就被称为垃圾标记阶段

在JVM内部,判断对象是否存活主要是通过两种方式:引用计数算法可达性分析算法

2.1.1,引用计数算法

这种算法就是会为每一个对象保存一个引用计数器属性,如对于一个对象A,只要有任何一个对象引用了A,那么这个A对应的计数器就会加1,当引用失效计数器就会减1。只有引用计数器的值为0时,表示不再被任何变量引用,那么该对象就会被标记,后续会根据是否标记进行回收。

优点是实现比较简单,垃圾对象便于识别,并且其效率比较高;缺点是需要单独的字段存储计数器,需要一定的空间开销,并且计数器需要加减运算操作,增加了时间开销,最主要的是无法处理循环引用的问题,因此Java并没有选择这种算法。

public class A{public A a = null;List<A> list = new ArrayList();public static void main(String[] args) {A objectA = new A();A objectB = new A();//这两个对象相互引用,俩个计数器都+1,导致无法回收objectA.a = objectB;objectB.a = objectA;}}

2.2.2,可达性分析算法

相对于引用计数器而言,可达性分析算法不仅具有简单和执行高效等特点,更重要的是可以有效地解决这个循环引用的问题,从而防止出现内存泄漏

通过上图可知,可达性分析算法是以根对象集合为起点,按照从上到下的方式搜索根对象集合所连接的目标是否可达,内存中的存活对象都会被根对象集合直接或者间接连接着,如GC roots和obj1是直接连接着的,和2,3,4是间接连接着的,这整个连接路径被称为引用链。如果目标对象没有任何引用链相连,如6,7,8,则是不可达的,就意味着该对象以及死亡,可以标记为垃圾对象。

在java中,常被用作GC对象的有以下几种:方法中的参数、局部变量、静态属性、字符串常量池引用、同步锁持有的对象、基本数据类型对应的Class对象、异常对象、本地缓存等等。

在使用这个可达性分析算法来判断内存是否可以进行回收,在分析工作时必须在一个能保障一致性的快照中进行,不允许在分析时出现动态的垃圾的增加等,这样才能保证分析结果的准确性。但是也正是因为这个快照的问题,让stw的出现不可避免。

3,JProfiler查看GC Root

查看GC Root有好几种方式,如使用MAT等,但是MAT要涉及到eclipse这些,因此废弃MAT。这里主要是通过这个 JProfiler 这个工具,可以直接在idea中搜索这个插件安装,然后重启idea即可。

除了这里安装之外,最好再安装一个 .exe 的可执行文件,可以参考黄莹这位大佬写的,这里面有免费JProfiler下载: https://blog.csdn.net/weixin_42311968/article/details/120726106 下载完之后一直点下去,安装,我这里是先使用免费10天的。

安装完成之后,再回到idea,点击右上角的JProfile的图标

然后在提示框中,输入刚刚 .exe安装路径下面的bin目录下面的 .exe,然后保存即可,再次点击就可以直接使用了

//如我这边安装目录是在D盘下,因此找到这个路径下面的 JProfiler.exe 文件即可D:\environment\jprofiler\jprofiler11\bin

然后下一步也是那个大佬里面写的,将JVM exit action的参数改成如以下图所示即可。

再点击ok之后,那么这个画面就有了,工具基本就可以使用了

一段时间之后,就会出现如下画面,其画面是动态的

在Live memory下的All Objects中监视着所有的内存情况,和之前谈到的JVisualVM的功能都是类似的,如之前谈到查看这个字符串常量池到底存在哪就是通过这个方式举的例子。

如果出现OOM,可以直接通过查看这个Heap Walker目录下的对象信息,来确定是哪个对象发生的OOM,以及是否出现大对象等问题。

4,垃圾回收相关算法

上面讲述了两种方式找出垃圾,接下来就需要将找出的垃圾给清除掉,释放无用对象的内存空间,以便有足够的可用内存为新对象的分配。在jvm中,比较常见的三种垃圾回收算法有:标记清除算法、复制算法、标记压缩算法

4.1,标记清除算法

当堆中有效的内存空间被耗尽的时候,会停止整个程序,简称stw(stop the world),这样可以防止在标记回收的和清除的时候又有新的垃圾出现。

在这里面主要进行两箱工作,第一项是标记,第二项是清除。标记是从根节点开始,标记所有的引用对象,一般是可达对象;清除是从头到尾线性遍历,发现某个对象没有标记为可达对象,则将其回收。

如上图,绿色部分表示存活对象,黑色表示垃圾对象,白色表示空闲对象,然后从根节点出发,判断对象是否可达,从而确定对象是否需要被回收,最后将黑色对应的对象回收即可,从而实现这个标记清除算法。

该算法的优点是简单易理解。但是缺点也很明显,效率较低;并且在GC的时候,需要停止整个应用程序(stw),用户体验差;最主要的是会产生大量的内存碎片,因此在内部需要维护一个空闲列表。这里的清除并不是直接将对象清除,而是将要清除对象的地址加入到空闲列表里面,然后记录指向要被清除对象的指针,后面来新的对象之后,则将这个空闲列表记录的指针指向新来的这个对象。

4.2,复制算法

针对标记清除的算法的缺陷,如会产生内存碎片,因此这种复制算法诞生。

其核心思想就是将内存空间分成两块,每次只使用其中的一块,在垃圾回收的时候将正在使用的内存中的存活的对象复制到未被使用的内存中,之后再将正在使用的内存中的对象清除,再交换两个内存的角色,最后完成垃圾回收

这里的复制是将完整的对象复制,在新生代中存活区的s0区和s1区就是实现了这种算法,经典以空间换时间。并且在后期分配对象时,可以直接使用指针碰撞算法。

复制算法的优点是:没有标记和清除的过程,简单实现,运行高效,并且解决了碎片化的问题。

复制算法的缺点是:需要两倍的内存空间,如果垃圾多,那么需要移动的次数也多,影响效率。

4.3,标记整理算法

复制算法效率虽然高,但是更加的适合新生代中使用,因为那里的对象朝生夕死,那么需要复制移动的对象就不会太多,也就不会太影响效率。但是复制算法不适合在老年代使用,因为里面的对象基本是存活的,那么真要复制移动起来,那么就会严重影响效率,那么这种算法的代价就会比较高。

而标记清除算法会产生垃圾碎片,显然也不能在这个老年代使用,因为如果出现一个大对象这就可能出现放不下的情况,因此就出现了一种新的算法,标记整理法。这种算法就是在标记清除这种算法之上进行了优化的一种算法,从而解决这种内存碎片的问题。

这种核心思想就是通过根节点开始标记所有能被引用的对象,然后将所有存活的对象按顺序排放,再清理掉没被引用的垃圾。标记整理算法的最终效果就是等同于标记清除之后,再进行一次碎片管理,一次也可以称为 标记-清除-压缩 算法,该算法也不需要使用空闲列表来记录碎片。

标记整理算法的优点是:解决标记清除碎片化问题,复制算法空间问题

标记整理算法的优点是:效率低于复制算法,移动对象的同时需要移动对象引用,移动过程需要stw

4.4,三种算法总结

从效率上来说,复制算法的效率最高,但是会浪费大量的内存。因此兼顾速度,空间开销,是否需要移动对象三个指标来说,标记整理算法相对来说更为稳定,但是效率上不尽人意,因为他比复制算法多了一个标记阶段,比标记清除算法多了一个整理内存的阶段。

5,垃圾回收相关概念

5.1,System.gc() 的理解

默认情况下,通过这个System.gc()的调用,就会显式的触发这个Full GC,同时对这个老年代和新生代进行回收,尝试释放被丢弃的对象所占用的内存。

但是这个System.gc()会调用一个附带的免责声明,就是说这个确实可以触发这个Full GC,但是垃圾回收器会不会做出具体的响应,已经响应的时间是否即时很难保证,有可能并不会响应这次请求,也可能在很长时间后才触发,导致想清除的对象复活或者迟迟不能回收导致OOM的情况。

垃圾回收一般是可以自动进行的,无需手动触发,否则就太过于麻烦了。但是在测试性能的基准的时候,可以在运行期间调用这个System.gc()。

/** * @author zhenghuisheng * @date : 2023/4/19 */public class Test {public static void main(String[] args) {User user = new User();//提醒Jvm进行垃圾回收System.gc();//强制执行//System.runFinalization();}//触发了垃圾回收就会调用这个finalize()@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("触发了垃圾回收");}}

通过上述代码可以得知,在System.gc()之后,有时会触发有时不会触发这个full GC,但是使用这个System.runFinalization()方法时可以保证一定会触发这个 FULL GC的,因为他是强制执行的。

5.2,内存溢出和内存泄漏

5.2.1,内存溢出(OOM)

由于GC的技术一直不断地完善和发展,因此在一般的情况下是不会出现OOM的,除非应用程序所占的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,这样才可能出现OOM。GC会进行各种年龄段的垃圾回收,在出现OOM之前也会触发一次FULL GC的操作,这时候会回收大量的内存,如果在回收大量的内存之后还不够用,那么就会出现OOM的问题。在java文档中对OutOfMemoryError的解释是这样的:没有空闲内存,并且垃圾收集器也无法提供更多的内存

而内存不够的原因,有可能是Java虚拟机堆内存设置的不够;也可能是创建了大量的大对象,并且对象被引用着,不能被回收,或者对象的大小直接超过堆内存的对大值。

5.2.2,内存泄漏

指的是对象不再被程序使用,但是GC又回收不了这些对象,就被称为内存泄漏。如一些静态对象,其生命周期比较长,但是这些对象关联了一些只用一次的对象或者一些资源对象,而资源对象没关,如mysql连接等,总而导致出现这个内存泄漏。

5.3,Stop The World

简称STW,指的是在触发这个GC时间之后,会产生程序的停顿,就是会让全部的用户线程暂停,没有任何响应,有点像卡死的感觉。在触发这个STW时,需要保证其工作在一个快照中进行,从而保证数据的一致性,如果在分析过程中出现对象还在不断的动态变化着,则最后的分析结果的准确性很难保证。

在被STW中断的应用程序会在GC之后恢复,然而频繁的中断会让用户感觉到卡顿的情况,让用户的体验不友好,因此在后续的优化中,STW就是重点要关注的对象。并且STW是在后台自动的发起和自动的完成的,会在用户不可见的情况下,强行的把用户正常的线程给全部停掉。因此在开发中也要少用System.gc(),否则也容易触发这个STW。

5.4,引用

当内存空间还足够时,可以保留在内存中,如果内存空间在垃圾收集之后还是很紧张,则可以抛弃这些对象,这些对象就被称为引用。

强引用(StrongReference):指在代码中普遍存在的引用赋值,如通过new一个对象,无论在任何情况下,只要强引用的关系还在,垃圾收集器就永远不会回收掉引用的对象。强引用的对象基本是可触及的,即rootGc是可达的,同时强引用也是内存泄漏的主要原因之一。

StringBuffer sbu = new StringBuffer("zhenghuisheng");

软引用(SoftReference):在系统将要发生内存溢出之前,会将这些回想列入回收范围之中进行第二次回收,如果第一次回收之后(回收GC Root不可达的对象)还没有足够的空间,那么第二次回收就会回收这些对象,第二次回收之后还是内存空间不足,那么就会抛出内存溢出的异常。如一些缓存,就是典型的使用了软引用

//声明一个强引用Object obj = new Object();//实例化一个软引用SoftReference<Object> sf = new SoftReference(obj);obj = null;

弱引用(WeakReference):只被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾回收器工作时,无论空间是否足够,都会被回收。弱引用也可以用来作为缓存使用

//声明一个强引用Object obj = new Object();//实例化一个弱引用WeakReference<Object> sf = new WeakReference(obj);obj = null;

虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚拟引用来获取一个对象的实例。设置一个虚引用关联的唯一目的是在这个对象被收集器回收时收到一个系统通知

//声明一个强引用Object obj = new Object();//引用队列ReferenceQueue ReferenceQueue = new ReferenceQueue();//实例化一个虚引用PhantomReference<Object> sf = new PhantomReference(obj,ReferenceQueue);obj = null;

总结来说:强引用默认是不回收,软引用是内存不足则回收,弱引用是发现即回收,虚引用是用于对象回收追踪

如若转载,请附上转载链接地址:https://blog.csdn.net/zhenghuishengq/article/details/130261481