JVM垃圾回收机制(GC)

目录

GC的作用:

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

内存泄露和内存溢出 

内存泄露 

内存溢出 

GC(垃圾回收的劣势) 

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

垃圾回收的过程: 

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

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

引用计数的缺陷 

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

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

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

GCRoots是有哪些

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

第二阶段:回收垃圾 

1、标记清除 

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

2、复制算法 

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

3、标记整理 

分代回收 


GC的作用:

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

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

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

内存泄露和内存溢出 

内存泄露 

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

内存溢出 

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

GC(垃圾回收的劣势) 

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

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

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

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

垃圾回收的过程: 

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

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

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

比如:

public static void fun(){
    
    Test t1=new Test();
    Test t2=t1;
 }

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

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

引用计数的缺陷 

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

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

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

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

比如我们要找宝藏:

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

比如说这样一个类:

class Test{
    Test test=null;
}

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

public class TestDemo {
    public static void main(String[] args) {
        Test t1=new Test();
        Test t2=new Test();
    }
}

这个时候的引用对象图:

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

public class TestDemo {
    public static void main(String[] args) {
        Test t1=new Test();
        Test t2=new Test();
        t1.test=t2;
        t2.test=t1;
    }
}

这时的引用对象指向:

直观一点:

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

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

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

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

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

 

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

GCRoots是有哪些

1、上的局部变量;

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

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

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

不会

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

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

第二阶段:回收垃圾 

回收垃圾有三种策略:

1、标记清除

2、复制算法

3、标记整理

1、标记清除 

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

 

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

2、复制算法 

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

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

 

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

3、标记整理 

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

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

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

分代回收 

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

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

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

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

分代回收过程:

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

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

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

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

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

猜你喜欢

转载自blog.csdn.net/m0_67995737/article/details/130022596