JVM垃圾回收机制(GC)

不念不忘少年蓝@ 2024-03-24 16:14 158阅读 0赞

目录

GC的作用:

申请内存的时机和释放内存的时机

内存泄露和内存溢出

内存泄露

内存溢出

GC(垃圾回收的劣势)

GC(垃圾回收) 的工作过程

垃圾回收的过程:

第一阶段:找垃圾/判定垃圾

方案一:基于引用计数(非Java)

引用计数的缺陷

1、内存空间浪费严重(空间利用率低)

2、 会出现循环引用的问题

方案二:可达性分析(Java)

GCRoots是有哪些

一个引用置为null之后,它之前指向的对象会立刻被回收吗?

第二阶段:回收垃圾

1、标记清除

标记清除的问题:释放的内存碎片化(内存不连续),影响程序运行的效率

2、复制算法

复制算法问题: 空间利用率低(一半),开销大(垃圾少时)

3、标记整理

分代回收


GC的作用:

GC:Garbage Clean(垃圾回收),我们在平时写代码的时候经常会进行申请内存,new操作,创建变量等等,但是内存是有限的,不断的申请会让内存耗尽,为了解决内存的消耗问题,引入了GC,这样就可以回收一些不用的内存,释放更多的空间出来。

申请内存的时机和释放内存的时机

申请内存的时机是比较好确定的,比如我们new,创建变量等等,但是什么时候不用这些变量就不容易知道。如果我们释放内存太早,但是后面还要用,那就尴尬了,如果我们释放的太晚了,内存不够后面申请了也是不行的。由于这些机制,就容易出现一些问题,常见的有内存泄露和内存溢出问题。

内存泄露和内存溢出

内存泄露

如果申请人在申请内存的过程中申请的内存越来越多,最后导致无内存可用的情况,这种现象就是内存泄露,垃圾回收就可以让我们程序猿不用关心内存泄露的问题,但是GC还是有一定的劣势的

内存溢出

内存溢出和上述问题没有必然联系,内存溢出指的是申请内存,没有足够内存提供给使用,比如一个long类型的数据申请int类型的空间大小,这就会导致内存溢出。

5083a54da9074b068954fcbdc38ade97.png

GC(垃圾回收的劣势)

1、引入额外的开销(消耗的资源更多了)

2、影响程序运行的速度(并且GC还会出现STW(stop the work)问题,这也是C++不引入GC的重要原因,C++追求速度到极限)

GC(垃圾回收) 的工作过程

首先JVM的内存区域划分为程序计数器,栈,堆,方法区(元数据区),其中栈中内存会自动回收,不需要GC,GC主要作用的区域就是我们的堆区,堆区存放着大量我们new出来的对象,GC要回收的对象都是些没有使用的,但是占着内存空间的对象。

fc95ec8ad0fc41b5a97ffc9405074af0.png

垃圾回收的过程:

第一阶段:找垃圾/判定垃圾

方案一:基于引用计数(非Java)

这个方案就是引入一小块的内存空间,用来存放有多少个引用指向该对象,如果引用的数量为0了就代表可以进行回收。

比如:

  1. public static void fun(){
  2. Test t1=new Test();
  3. Test t2=t1;
  4. }

这个对于new Test()这个对象的引用计数就是2,当fun方法执行完毕的时候,栈上的栈帧就会消失,然后对new Test()的引用计数就会变成0,这个时候就可以GC进行回收了。

f6162df0645240a5a409c4b1394b26bb.png

由此可见引用计数的缺陷很明显

引用计数的缺陷

1、内存空间浪费严重(空间利用率低)

使用引用计数,每次new一个对象的时候,都要引入一个计数器,这个计数器也是需要占据空间的,并且有时候占据的空间也不小,比如当我们的对象是4字节,计数器也是4字节的时候,这样的情况就非常的浪费空间。

5cbb5220fbb84ee99230f756582ab41f.png

2、 会出现循环引用的问题

通过一个例子来说明什么是循环引用:

比如我们要找宝藏:

6b7a38843d1a48098eab22bdaf60bb2b.png

如果这个例子不是很理解,我们用代码举例:

比如说这样一个类:

  1. class Test{
  2. Test test=null;
  3. }

在测试类中创建该类实例:

  1. public class TestDemo {
  2. public static void main(String[] args) {
  3. Test t1=new Test();
  4. Test t2=new Test();
  5. }
  6. }

这个时候的引用对象图:

a76b4bceb25c48c089d03c633bb89888.png

这时我们修改引用的指向:

  1. public class TestDemo {
  2. public static void main(String[] args) {
  3. Test t1=new Test();
  4. Test t2=new Test();
  5. t1.test=t2;
  6. t2.test=t1;
  7. }
  8. }

这时的引用对象指向:

e85a0fc4d1d840608d2b196e79945243.png

直观一点:

33dba4119f654810bd08176442d3ab0d.png

这个时候如果我们将t1和t2置为null,这个时候这两个对象的引用计数就都会变成1,变成1之后相当于这样:

5952c28dede145bfa60b9545ed3a45a6.png两个对象互相引用,这就导致外界没有办法访问这两个对象(和上面的寻宝藏一样),所以这两个对象永远都没有办法回收,也永远不能够使用,这不是我们想要的结果 ,还会造成内存泄露。

所以Java中不使用引用计数的方式来判定垃圾。

方案二:可达性分析(Java)

可达性分析就是通过一个线程来定期的扫描整个内存中的对象,扫描的过程类似于深度优先搜索 (起始位置一般为GCroots),把所有可以到达的对象都标记一遍,带有标记的对象就是可达的,没有标记的对象就是不可达的,也就是垃圾。(可以避免循环引用

c7c4a08a961649d2a5d9cde9aaaa5e15.png

虽然说可达性分析避免了循环引用的问题,但是如果对象量比较大的情况下还是会花费大量时间进行搜索,比较消耗性能。

GCRoots是有哪些

1、上的局部变量;

2、常量池当中的引用指向的变量;

3、方法区当中的静态成员指向的对象。

一个引用置为null之后,它之前指向的对象会立刻被回收吗?

不会

一是因为即使一个引用置为空之后,并不代表这个对象就没有别的引用了。

二是因为可达性分析扫描是需要时间的,只有扫描过后判定是垃圾才会进行回收。

第二阶段:回收垃圾

回收垃圾有三种策略:

1、标记清除

2、复制算法

3、标记整理

1、标记清除

标记就是我们可达性分析的过程,标记完发现是垃圾的直接进行清除,释放内存即可

2325243ad8b44434856c39cda6e740b2.png

标记清除的问题:释放的内存碎片化(内存不连续),影响程序运行的效率

2、复制算法

复制算法简单来说就是把内存一分为二,然后把正常的对象复制到另一边。然后把垃圾的那一边全部释放掉。(避免了内存碎片化

5691b7750bc7496d82bb502f81628685.png

然后把左侧的内存全部释放:

3cdd4edaa10743e48ec063223a03a17b.png

复制算法问题: 空间利用率低(一半),开销大(垃圾少时)

3、标记整理

标记整理类似于数组中元素的移动,就是把不是垃圾的对象往前移动,是垃圾的往后移动,然后把垃圾一块回收。

c17e53ddb962450dbf3654705894ac1c.png

标记整理的策略开销也是比较大的。

上述的方案都是单一的,实际上JVM中的方案不是单一的,而是结合上述方案的的策略,称为“分代回收”。

分代回收

分代回收就是指对对象进行分类,按照“年龄”分成不同的类别进行回收。

对象的年龄:每熬过一轮GC扫描,年龄加1,年龄存储在对象头中

存储对象的内存区域划分为新生代老年代

新生代中又分为了伊甸区和幸存区(幸存区有两个)

分代回收过程:

1、刚产生的对象放在伊甸区

2、熬过一轮GC,拷贝到幸存区利用复制算法),大部分对象熬不过一轮GC

3、在后续的GC中幸存区的对象在两个幸存区来回进行拷贝(采用复制算法),进行对象的淘汰

4、经过了多轮的GC后,如果一个对象还是没有被淘汰,那么就会被放入老年代。对于老年代的对象来说,GC扫描的次数就远低于新生代了。同时,老年代当中采用的就是“标记——整理”的方式来回收。

ccd6a93a60874919906d5689e3f13a94.png特殊情况:一个对象特别大(占用内存特别多),不用经过多轮GC,直接进入老年代。(因为太消耗性能了)

发表评论

表情:
评论列表 (有 0 条评论,158人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM 垃圾回收机制GC

    目前为止,jvm已经发展处三种比较成熟的垃圾收集算法:1.标记-清除算法;2.复制算法;3.标记-整理算法;4.分代收集算法 1.        标记-清除算法 这种垃圾回