JVM的垃圾回收机制(GC)

简介: JVM的垃圾回收机制(GC)

一、什么是垃圾回收?

垃圾回收,回收的是内存。JVM其实是一个进程,一个进程会持有很多硬件资源,比如:CPU,内存,硬盘,带宽资源等。系统的内存总量是一定的,程序在使用内存的时候,必须先得申请,才能使用,使用完毕后还要释放。


从代码编写的角度看,内存的申请时机是非常明确的,但是内存的释放时机在很多时候是不太明确的。这个时候就给内存的释放带来了一些困难,典型的问题就是,这个内存是否还要继续使用?


像C/C++这样的编程语言,内存释放,是纯手工的,全靠程序员手动释放。比如,在C语言中,如果malloc出来的对象,不手动调用free,那么这个内存就会一直持有。此时,内存的释放就全靠程序员自己来控制,如果忘了释放(在该释放的时候没有释放),那么很有可能会带来“内存泄漏”这样的问题。一直申请不释放,导致系统可用的内存越来越少,直到耗尽,如果再想要申请内存,就申请不到了。所以,内存泄漏,一向成为了程序员幸福感的头号杀手!


既然内存泄漏有这么大的弊端,肯定有相应的办法来反制。比如,在C++中,就采取了智能指针的方式,在java中采取的方案就是“垃圾回收”机制。对于java来说,代码中的任何地方都可以申请内存,然后由JVM统一进行释放,具体来说,就是由JVM内部的一组专门负责垃圾回收的线程来进行这样的工作的。


JVM垃圾回收的优点:


能够非常好的保证不出现内存泄漏的情况,但是也不是100%保证,就是再厉害的机制也顶不过程序猿自己瞎搞。


JVM垃圾回收缺点:

1.需要消耗额外的系统资源

2.内存的释放可能存在延时

3.可能会导致出现STW问题(stop the world)(在java的世界中,其实前辈们已经做出了很多的努力来改进这个问题,目前能够把STW控制在1ms内)

二、java的垃圾回收,要回收的内存是哪些?

JVM中的内存分为了好几个区域,有堆、方法区、栈和程序计数器。在这几个区域中,堆占据的内存空间是最大的,所以在java的垃圾回收机制中,咱们日常讨论的垃圾回收,主要是指堆上内存的回收。

三、回收堆上的内存,具体是回收什么?

在堆上,是new出了很多的对象,此时针对堆上的对象,也分成三种:完全使用、完全不使用、一半要使用一半不使用。对于完全不使用,这就是我们要回收的东西。java中的垃圾回收,是以“对象”为基本单位的,一个对象要么被回收,要么不被回收,不会出现一个对象被回收一半的情况。

四、垃圾回收到底是怎么回收的?

垃圾回收的基本思想是先找出垃圾,再回收垃圾。一般情况下时把再也不会被使用到的对象进行垃圾回收,如果要是把正在使用的对象进行垃圾回收,这是一个非常可怕的结果。对于回收少了这样的问题来说,回收多了或回收错了显然是更严重的问题。对于GC来说,判定垃圾的原则,宁可放过,也不能错杀.........

如何判定垃圾?

单说GC的话,判定垃圾有两种典型的方案:引用计数、可达性分析

1.引用计数

在对象里面包含一个单独的计数器,随着引用增加,计数器就自增,随着引用减少,计数器就自减。

Test a = new Test();

此时认为new Test()这个对象就有一个引用指向它。

Test b = a;

此时就有a和b两个引用都指向这个对象。引用计数,就是通过一个变量来保存当前这个对象被几个引用来指向。

Test a = new Test();
Test b = a;
a = null;
b = null

当这两个引用都指向 null 的时候,然后这个对象就没有被指向了,此时这个对象就认为是垃圾了(引用计数为0)。


引用计数的优点:

规则简单,实现方便,比较高效(程序运行效率比较高)。

引用计数的缺点:

1.空间利用率比较低,针对大量的小对象,比较浪费空间。(比如,一个对象4个字节,也需要4个字节的计数器)

2.存在循环引用的问题(致命问题)

有些特殊的代码下,循环引用会导致代码的引用计数判断出现问题,从而无法回收。

class Test{
  Test t = null;
}
Test t1 = new Test();
Test t2 = new Test();
t1.t = t2;
t2.t = t1;


当执行下面这个操作:t1 = null; t2 = null;这一操作其实是销毁了两个引用,但是引用计数只减了1,没有了t1,就无法使用t1.t。

很多编程语言,虽然使用了引用计数这种机制,但是实际上都是改进过的引用计数,比如(Python,PHP)。在java中,并没有使用引用计数的这种方式,而是使用第二种机制:可达性分析。

2.可达性分析(java)

从一组初始的位置出发(比如二叉树),向下进行深度遍历,把所有能够访问到的对象都标记成“可达”(可以被访问到),对应的,不可达的对象就是垃圾。

JVM中采取的方案是:在JVM中存在一组线程,来周期性的进行上述的遍历过程。不断的找出这些不可达的对象,由JVM进行回收。

把可达性分析的初始位置称为“GCRoot”,主要对栈上的局部变量表中的引用、常量池里面的引用指向的对象,方法区中引用类型的静态成员变量进行标记。和引用计数相比,可达性分析确实要稍微麻烦一点,而且同时可达性分析的遍历过程的开销是比较大的。虽然是开销比较大,但是后面会有一些优化的手段。但是可达性分析带来了好处就是解决了引用计数的两个缺点,内存上不需要消耗额外的空间,也没有循环引用的问题。

已经知道哪些对象是垃圾了,具体怎么去回收呢?

在java的垃圾回收机制中,有一些经典的策略/算法:标记-清除、复制算法、标记整理以及分代回收。

1.标记-清除

白色是正在使用的对象,灰色是已经被释放的空间。虽然这个过程可以释放掉不用的空间,但是引入了额外的问题:内存碎片。


如果内存中的内存碎片很多很多,那么此时你去申请一块小的内存还好,但是想要很大的一块连续内存空间,可能会申请失败。内存碎片问题,如果一直累积下去,就会导致出现系统上看起来空闲内存挺多的,但是实际上申请连续的内存空间是申请不到的。内存碎片问题,在“频繁申请释放”的场景中,尤为严重。  2.复制算法

为了解决内存碎片问题,就引入了复制算法。

复制算法把整个内存分为了两个部分,一次只用一个部分,1和3要被回收了, 于是就把剩下的2和4拷贝到另外一侧,然后再整体回收这一整块空间。

使用复制算法,可以非常有效的避免出现内存碎片问题。但是复制算法也有一些缺点:

可用的内存空间只有一半;如果要回收的对象比较少,剩下的对象比较多,那么复制的开销就比较大了。复制算法只适用于对象会被快速回收,并且整体内存不大的场景下。

3.标记整理

为了能够解决复制算法的内存空间利用率低的问题,就引入了标记整理的策略。标记整理,就有点类似于“顺序表删除元素的搬运过程”。

这样的操作,既可以有效的避免内存碎片,也可以提高空间的利用率。但是在这个搬运的过程中,也是一个很大的开销,这个开销比复制算法里面的复制对象的开销还要大。

4.分代回收

在实际中的垃圾回收算法,是结合了以上的三种方式,取长补短,引入了分代回收的策略。


在分代回收中,把内存中的对象分成了几种情况,每一种情况采用不同的回收算法。


那么是如何进行分代的呢?


是根据对象的“年龄”的年龄来进行划分的。在JVM中,在进行垃圾回收扫描(可达性分析)也是周期性的,这个对象每次经历了一个扫描周期,就认为是长了一岁。就根据这个对象的年龄,就把整个内存进行划分。年龄短的放在一起,年龄长的放在一起。根据不同的年龄对象,就可以采用不同的回收算法了。

分代回收的过程:

1.一个新的对象,诞生于伊甸区。

2.如果活到一岁的对象(该对象经历了一轮GC还没回收)就拷贝到幸存区。根据经验规律,绝大部分对象都是熬不过一轮GC的,所以进入幸存区的对象不是很大,这里的拷贝开销就不是很大。


3.在幸存区中,对象也要经历若干轮GC。每一轮GC逃过的对象,都通过复制算法拷贝到另外的幸存区里。在这两个幸存区的对象经过来回拷贝,每一轮都会淘汰一批对象。


4.在幸存区中,熬过一定轮次的GC,JVM就认为这个对象未来还会更持久的存在下去,于是就把这样的对象拷贝到老年代中。


5.进入老年代的对象,JVM认为都是属于能够持久存在的对象。但是这些对象也需要使用GC来扫描,但是扫描的频次就大大降低了,老年代这里通常使用的是标记整理算法。


其实在某些特殊情况下,如果一个对象特别大,为了避免大的开销,那么也会直接进入老年代的。


相关文章
|
4月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
458 55
|
9月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
232 27
|
6月前
|
存储 算法 Java
G1原理—5.G1垃圾回收过程之Mixed GC
本文介绍了G1的Mixed GC垃圾回收过程,包括并发标记算法详解、三色标记法如何解决错标漏标问题、SATB如何解决错标漏标问题、Mixed GC的过程、选择CollectSet的算法
G1原理—5.G1垃圾回收过程之Mixed GC
|
4月前
|
缓存 算法 Java
JVM深入原理(八)(一):垃圾回收
弱引用-作用:JVM中使用WeakReference对象来实现软引用,一般在ThreadLocal中,当进行垃圾回收时,被弱引用对象引用的对象就直接被回收.软引用-作用:JVM中使用SoftReference对象来实现软引用,一般在缓存中使用,当程序内存不足时,被引用的对象就会被回收.强引用-作用:可达性算法描述的根对象引用普通对象的引用,指的就是强引用,只要有这层关系存在,被引用的对象就会不被垃圾回收。引用计数法-缺点:如果两个对象循环引用,而又没有其他的对象来引用它们,这样就造成垃圾堆积。
145 0
|
4月前
|
算法 Java 对象存储
JVM深入原理(八)(二):垃圾回收
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为StopTheWorld简称STW,如果STW时间过长则会影响用户的使用。一般来说,堆内存越大,最大STW就越长,想减少最大STW,就会减少吞吐量,不同的GC算法适用于不同的场景。分代回收算法将整个堆中的区域划分为新生代和老年代。--超过新生代大小的大对象会直接晋升到老年代。
98 0
|
6月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
6月前
|
存储 算法 Java
G1原理—6.G1垃圾回收过程之Full GC
本文详细探讨了G1垃圾回收器对Full GC(FGC)的优化处理,涵盖FGC的前置处理、整体流程及并行化改进。重点分析了传统FGC串行化的局限性以及G1通过Region分区和RSet机制实现并行标记的优势,包括任务窃取提升效率、跨分区压缩以生成空闲Region等技术细节。此外,文章还介绍了G1的新特性——字符串去重优化,通过判断char数组一致性减少重复字符串占用内存,从而提升内存使用效率。总结部分全面回顾了G1在FGC中的各项优化措施及其带来的性能改善。
G1原理—6.G1垃圾回收过程之Full GC
|
6月前
|
存储 算法 Java
G1原理—4.G1垃圾回收的过程之Young GC
本文详细解析了G1垃圾回收器中YGC(Young Generation Collection)的完整流程,包括并行与串行处理阶段。内容涵盖YGC相关参数设置、YGC与Mixed GC及FGC的关系、新生代垃圾回收的具体步骤(如标记存活对象、复制到Survivor区、动态调整Region数量等),以及并行阶段的多线程操作和串行阶段的关键任务(如处理软引用、整理卡表、重构RSet)。
G1原理—4.G1垃圾回收的过程之Young GC
|
10月前
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
230 28
|
9月前
|
算法 网络协议 Java
【JVM】——GC垃圾回收机制(图解通俗易懂)
GC垃圾回收,标识出垃圾(计数机制、可达性分析)内存释放机制(标记清除、复制算法、标记整理、分代回收)