java GC垃圾回收机制学习笔记

参考链接:

Java -- 深入浅出GC自动回收机制 - 阿呆哥哥 - 博客园

Java系列笔记(3) - Java 内存区域和GC机制 - Daniel·广 - 博客园

Java GC - 垃圾回收机制 - mikevictor - 博客园

https://blog.csdn.net/qq_36314960/article/details/79923581

Java垃圾回收(GC)机制详解 - 平凡希 - 博客园

and so on。。。。。。

在之前的面试中经常会被问到你对GC的理解,这次就认真地学习一下GC,通读了几篇讲述详细且易于理解的前辈们的博客之后,按照自己的思路梳理一下,便于后期回顾。(博客总结借鉴于各位大佬的博客,只是自己综合了很多人对同一个知识点阐述中最全面的理解)

Java  GC(Garbage Collection)垃圾回收机制。是一种自动的存储管理机制,当一些被占用的内存不再需要时,就应该予以释放,让出空间,这种存储资源管理称为垃圾回收。

先看一下JVM中内存的结构

线程共享的区域:堆区和方法区。

线程私有的区域:程序计数器、虚拟机栈、本地方法栈。

程序计数器:程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器,它只是记录了当前指令的地址,所以这个区域就不存在内存溢出的情况。由于程序计数器记录的是当前线程的行号,所以该区域是线程私有的。

虚拟机栈:每个线程创建的同时都会创建虚拟机栈,虚拟机栈的栈元素是栈帧,每个方法在执行的同时会被创建一个栈帧(一个线程中可以有多个方法,所以一个虚拟机栈中可以有多个栈帧)。栈帧可以理解为一个方法的运行空间,它主要由两部分组成:一部分是局部变量表(存放方法中定义的局部变量以及方法的参数),另一部分是操作数栈(存放操作数)。还有动态链接、方法出口等。当方法被调用时,栈帧在虚拟机栈中入栈,当方法执行完成时,栈帧出栈。

本地方法栈:本地方法栈在作用、运行机制、异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法,本地方法栈是执行native方法,该区域用于存储每个native方法调用的状态。

堆:在JVM所管理的内存中,堆区是最大的一块,也是Java  gc所管理的主要内存区域。堆区由所有线程共享,在虚拟机启动时创建。该区域的存在是为了存储对象实例,原则上讲,所有对象都是在堆区域上分配内存(也有一些技术可以在栈上存储对象)。通过new创建的对象和使用反射创建的对象都是存储在堆中的。

方法区:在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但是实际上方法区并不是堆。在sun jdk中该区域被称为永久代。方法区是线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

在Java   JVM中进行垃圾回收的地方有两个:堆和方法区,也就是包含三部分:年轻代、年老代、永久代。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以不在JVM  gc的管理范围。

在方法区中垃圾收集主要是回收废弃常量和无用的类,在Java虚拟机规范中说过可以不要求在方法区实现垃圾回收,在方法区中回收的条件比较苛刻而且回收的空间还远小于新生代回收的空间,所以很少在方法区中回收垃圾。

垃圾回收的重要场所就是堆,在堆中存储了很多对象,而有些对象已经不再被使用,那么就需要将其所占用的内存空间回收,以备分配给其他对象。

Java内存分配机制:这里所说的内存分配主要是指在堆上分配。堆区分为两个物理区域:年轻代和年老代(年轻代与老年代的比例是1:2,这个比例可以通过参数设定)。年轻代又被分为三部分:Eden、Survivor1(From Survivor)、Survivor2(To Survivor),这三部分的空间占比分配在JVM默认是8:1:1(可以通过参数设定)。

空间分配:

大部分对象刚创建时在年轻代中分配空间(存活时间较长的对象会在多次执行GC后经过Survivor区转存到老年代),大对象(需要分配一块比较大的连续内存空间)直接在老年代中分配空间(一般在Survivor空间不足的情况下发生)。

年轻代:绝大多数刚创建的对象被分配在Eden区,Eden区是连续的内存空间,所以在该区域上分配内存的速度是很快的,Eden区空间不足时,会触发Minor GC(年轻代GC,发生的频率比较高,不一定等到Eden区满了才触发)。在年轻代中的大部分对象在创建后很快就不再使用(大多数对象的寿命都很短,甚至都不会活到Survivor),因此很快变得不可达,这时就会被Minor GC清理掉。我们每次在使用的时候使用的是Eden区和其中一个Survivor区,Suvivro1和Survivor2这两个区域必须有一个是空闲的(如果两个Survivor区中都有数据或两者都是空的,那就是你的系统出现了某种错误),也就是说年轻代中实际可用的空间只有90%。在触发Minor GC机制时,将Eden区中存活的对象复制到其中一个Survivor区(假设是Survivor1),然后清空Eden区。在每次Eden区执行GC时,都是将存活对象向同一个已使用的Survivor区(假设使用的还是前面提到的已被使用的Survivor1)中复制,如果当Eden区满的时候,恰好Survivor1也满了,那么这个时候就会把Eden区和Survivor1区的存活对象都复制到Survivor2中,然后清空Eden区和Survivor1区(如果Survivor2的空间装不下Eden和Survivor1的存活对象,那么就把存活对象放到老年代中,如果老年代空间也满了,就会触发Full  GC,也就是年轻代和老年代都进行回收。)。等到Eden区再满了就把存活对象继续复制到正在使用的Survivor2中,就这样来回重复,循环使用Survivor区中的一个。对象在经过每一轮的GC之后,对象的年龄就会加1,当年龄达到年龄阈值(默认是15)时,就会被移动到老年代(但JVM也并不是永远都要求对象的年龄必须达到年龄阈值才能晋升到老年代,如果在Survivor区中所有相同年龄(age)的对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无须等到达到年龄阈值)。

老年代:按照上面分析得到的老年代里面存放的对象有这么几种:1、需要分配连续内存空间的大对象。2、经过多次年轻代GC之后年龄达到年龄阈值的对象。3、Eden区和其中一个Survivor区的存活对象已经在另一个Survivor区中装不下的时候存储在老年代中。所以,老年代中的对象大部分是不容易被回收的对象,所以在老年代中发生GC的频率(Major GC)要低于年轻代中发生GC的频率。

而Full GC是清理整个堆空间(包括年轻代和老年代,同时也会清理方法区里面的对象,也就是永久代)。

那么还有一个问题:在执行Minor GC时,如果老年代的对象存在对年轻代对象的调用,那么还需要查询整个老年代以确定是否可以回收,这样做效率显然低。解决的方法是在老年代中维护一个512byte的块(“card table”),所有老年代对象引用年轻代对象的记录都记录在这里。执行Minor GC时,只需要来这里查就可以了,不用查询所有的老年代,这样性能就可以提高。

根据上面分析的我们可以得出:GC分为三种:Minor GC、Major GC、Full GC。

不同的GC机制会有不同的垃圾回收算法,但每一种GC算法中都会出现stop-the-world,这意味着,当stop-the-world发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。所以GC优化很多时候就是减少stop-the-world的发生。
常用的垃圾回收算法:

1.标记-清除算法(Mark-Sweep)

       用在老年代GC中。该算法是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想最简单。该算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。标记和清除的效率都不太高,且存在内存碎片。使用场景:对象存活率较高。示意图:

即清除后,黑色部分变成了绿色未使用状态。

这种算法操作简单,不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象较多的情况下极为高效,但是最大的缺点就是回收后没有对存活对象进行整理,导致产生内存碎片,碎片太多会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2.复制算法(Copying)

用在年轻代GC中。该算法是为了解决标记-清除算法中效率低的问题。将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这话一块内存用完了,就把这里面存活的对象复制到另一块中(复制到的那块内存是连续的),然后把原来使用的那一半内存中的所有对象都清理掉。这样,每次只需要对一半的空间进行回收,在内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。使用场景:对象存活率较低。示意图:

这种算法虽然不容易产生内存碎片,但是对内存空间的使用做出了高昂的代价,可用的内存缩减了一半。在存活对象较少的时候有优势,如果存活对象太多,那么复制起来的效率就会降低。(老年代中的对象都是不易被回收的对象,如果使用该算法需要进行大量的复制,效率很低,所以老年代不使用这种算法)

3.标记-整理算法(Mark-Compact)

该算法是为了解决复制算法中内存使用率低的问题,而且也解决了对象存活率高的情况下复制效率低的问题。该算法与标记-清除算法的标记和清除阶段是一样的,但是在标记之后不是直接清除对象,而是将所有存活对象都向一端移动(并更新对应的指针),然后清理掉边界以外的内存。示意图:

4.分代收集算法

该算法并不是一种新的算法,而是在上述算法的基础上根据对象生命周期的不同进行了分代(将内存划分了几个区域:年轻代、年老代、永久代),然后再根据不同的代的特点采取上述合适的垃圾收集算法。

总结一下垃圾回收算法:

GC判断对象是否需要被回收的两个方法:

1.引用计数法

在这种方法中,堆中每一个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1.当任何其他变量被赋值为这个对象的引用时(即使用到该对象时)计数加1,当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1.任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。这种方法实现简单,判定效率高。但是,Java中没有使用这种算法,因为这种算法很难解决对象之间相互引用的问题。

2.可达性分析算法

这是Java虚拟机采用的判定对象是否存活的算法。通过一系列被称为“GC  Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就说明该对象是可被回收的。

 图中右侧的两个对象没有引用链,被当做是可回收对象。

可以被称作GC  Roots的对象包括以下几种:

虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。(至于原因下次继续研究)

但并不是没有在引用链中的对象都会被回收。它在被回收之前还会做两次标记。如果对象在进行可达性分析后没有引用链,那么它将会被第一次标记。第一次标记之后会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。在finalize()方法中没有重新与引用链建立关联关系,将会被进行第二次标记。第二次标记成功的对象将会真的被回收。如果对象在finalize()方法中重新与引用链建立了关联关系,那么就会逃脱本次回收,继续存活。

Java中的引用分类(摘自https://blog.csdn.net/linzhiqiang0316/article/details/80473906):

强引用:

       如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出OutOfMemoryError错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

软引用:

       在使用软引用时,如果内存的空间足够,软引用就能继续内使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。

弱引用:

       具有弱引用的对象拥有的生命周期更短。因为当JVM进行垃圾回收时,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以不一定能迅速发现弱应用对象。

虚引用:

       顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。

在上述判断对象是否可回收时所提到的引用都是指强引用。

方法区的垃圾回收:

方法区主要回收的内容包括两部分:废弃常量和无用的类。

如何判断废弃常量:以String常量abc为例,当我们声明了此常量,那么它就会被放到运行时常量池中,如果在常量池中没有任何对象对abc进行引用,那么abc这个常量就算是废弃常量而被回收。

如何判断无用的类:需要同时满足下面三个条件:
1.该类的所有实例都已经被回收。

2.加载该类的ClassLoader已经被回收。

3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

然后在了解垃圾收集器之前先了解一下JVM中server模式和client模式:

(参考链接:JVM client模式和Server模式的区别_tang_123_的博客-CSDN博客Java虚拟机6:内存溢出和内存泄露、并行和并发、Minor GC和Full GC、Client模式和Server模式的区别 - 五月的仓颉 - 博客园

我们使用的JVM(HotSpot)如果不显式指定是-Server模式还是-client模式,JVM能够根据一个原则去判断:J2SE5.0检测的根据是至少2个CPU和最低2GB内存。如果是,则虚拟机以Server模式运行,该模式更注重编译的质量,启动速度慢,适合在服务器环境下使用。如果不是,则虚拟机会以client模式运行,该模式更注重编译的速度,启动速度快,更适合在客户端的版本下使用。从上述的参考链接中截出的图:

从图中可以看出红色的Server  JVM的性能比黄色的Client  Server的性能要好。尤其在Method  Call中表现明显。

查看虚拟机运行模式的方法:

1.在cmd窗口中运行java  -version,查看本地安装的虚拟机信息:

2.在使用eclipse时,一般使用的都是工具自带的JRE,虚拟机并不是本地安装的虚拟机,那么可以通过下面的语句来查看虚拟机信息:

垃圾收集器:

垃圾收集器是GC的具体实现,这其中也会用到stop the world。先看一下并发与并行的区别,简单总结就是:

串行:单线程,用户线程暂停工作。(Serial)

并行:多线程,多个GC线程并行工作,用户线程暂停工作。(Parallel)

并发:多线程,用户线程与GC线程同时执行。(CMS)

我们使用的虚拟机是HotSpot,涉及到了七种垃圾收集器:

上半部分是年轻代收集器,下半部分是老年代收集器,如果收集器之间有连线,那么说明它们可以搭配使用。

七种垃圾收集器

Serial:单线程收集器,是client模式下默认的年轻代收集器。采用停止复制算法,进行垃圾回收时必须暂停用户的所有进程。优点是简单高效。在单CPU的环境中,由于没有线程交互的开销,专心做垃圾收集,所以在这种情况下相对于其他收集器是最高效地。

Serial Old:是Serial收集器的老年代版本,也是一个单线程收集器,使用“标记-整理”算法。它的主要意义就是在client模式下使用。该收集器执行时,其他用户线程暂停。

ParNew:是Serial收集器年轻代的多线程实现,但在单CPU的环境中绝对不会有比Serial收集器更好的结果。该收集器采用停止复制算法,用多个线程进行GC,并行,其他工作线程暂停,关注缩短垃圾回收时间,是server模式下虚拟机首选的年轻代收集器(其中一个很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS收集器是一款几乎可以认为有划时代意义的垃圾收集器,因为它第一次实现了让垃圾收集线程与用户工作线程基本同时工作)。

Parallel Scavenge:采用复制算法的多线程年轻代垃圾收集器,和ParNew收集器有很多相似的地方。但是Parallel Scavenge收集器的一个特点是它关注的目标是吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,因为良好的响应速度能够提升用户的体验。而高吞吐量则能以最高效地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。该收集器是虚拟机运行在server模式下的默认垃圾收集器。

Parallel Old:是Parallel Scavenge收集器的老年代版本。多线程,并行,使用标记-整理算法,在该收集器执行时,需要暂停其它线程。

CMS(Concurrent Mark Swep):是一个比较重要的收集器,现在应用非常广泛,CMS是以获取最短回收停顿时间为目标,这使得它很适合用于和用户交互的业务。该收集器是基于标记-清除算法实现的,会导致有大量的内存碎片产生,是老年代收集器。收集过程为四个步骤:1.初始标记(initial mark)。2.并发标记(Concurrent mark)。3.重新标记(remark)。4.并发清除(concurrent sweep)。初始标记和重新标记时会暂停用户线程。并发标记和并发清除时耗时较长,但允许和用户线程同时工作。该收集器是多线程,优点是并发收集,用户线程和GC线程同时工作,停顿小。

G1(GarbageFirst):是一款面向服务端应用的垃圾收集器,既可用于老年代也可用于年轻代。

总结年轻代与老年代所使用的收集器:

年轻代:Serial,ParNew,Parallel Scavenge,G1.

老年代:Serial Old,Parallel Old,CMS,G1.

对比七种收集器:

什么时候触发GC:

1.显式调用System.gc()时,会触发Full GC

2.老年代空间不足时,会触发Full GC

3.永久代空间不足时,会触发Full GC

4.Eden区满时,会触发Minor GC

以上是通过阅读一些前辈们的博客提取出来便于自己理解的总结,仍处于浅显的理解,之后会深入研究原理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QYHuiiQ

听说打赏的人工资翻倍~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值