JVM:垃圾回收

当我们编写Java应用程序时,我们通常不需要考虑内存管理的细节,因为Java虚拟机(JVM)会自动处理内存分配和垃圾回收。垃圾回收是JVM的一个重要功能,它负责在应用程序运行时自动回收不再使用的内存。在这篇技术博客中,我们将深入探讨JVM垃圾回收机制的工作原理,以及如何优化垃圾回收以提高Java应用程序的性能。

目录

什么是垃圾?

如何定位垃圾?

引用计数算法

根可达算法

常见垃圾回收算法

分代

分区

新生代和老年代

常用垃圾回收器

Serial

Parallel Scavenge

CMS

三色标记算法

特殊情况

第一种是CMS的解决方案:Incremental Update 

CMS解决方案的缺陷

第二种是G1方案:SATB Snapshot At the Beginning

Rset

实战操作

GC常用参数:

G1常用参数


什么是垃圾?

不管我们写什么语言,我们都需要向内存当中分配数据,在C语言当中,我们使用malloc方法来给变量分配内存。在C++中我们new一个对象来分配内存。然而,所有语言都会面临一个问题:当我分配了一块内存空间之后,我们需要把它回收掉。

在C/C++中我们一般称之为手工回收。也就是需要开发人员自己回收内存。手工回收在调试程序的时候有巨大的困难。比如假设我们忘记了回收内存,别的变量就无法再占有这块内存,这就造成了内存泄漏。再比如我们判断失误多回收了一次,及会造成不可预估的损失。

当我们再内存中分配之后再也用不到的空间,就是我们所谓的垃圾。由于C/C++是手工回收非常容易出错,所以java中设置一个“垃圾管理员”GC来完成回收任务。

如何定位垃圾?

当我们要回收内存当中的垃圾的时候,首先需要定位垃圾的位置,常用的垃圾定位算法有两种:

引用计数(reference count)和根可达(RootSearching)

引用计数算法

引用计数算法是一种简单的垃圾定位算法,它通过在对象中添加一个计数器来跟踪对象的引用数。当引用数为0时,对象就可以被回收。

JVM:垃圾回收_第1张图片

但是,这种算法存在一个问题,即无法处理循环引用的情况。例如,如果对象A引用了对象B,而对象B又引用了对象A,那么它们的引用计数永远不会为0,即使它们已经不再被程序使用了。

根可达算法

为了解决这个问题,JVM使用可达性分析算法。该算法从一组称为“GC Roots”的对象开始,然后通过这些对象的引用链来查找所有可达对象。所有不可达对象都可以被回收。GC Roots包括虚拟机栈中的对象、方法区中的类静态属性引用的对象以及本地方法栈中JNI(Native方法)引用的对象等。

JVM:垃圾回收_第2张图片

通过可达性分析算法,JVM可以正确地处理循环引用的情况,并且可以准确地确定哪些对象可以被回收,从而有效地管理内存。

常见垃圾回收算法

常见的垃圾回收算法有以下几种:

  1. 标记-清除算法(Mark and Sweep):该算法分为两个阶段,首先标记所有活动对象,然后清除所有未标记的对象。该算法的缺点是碎片化,会产生内存碎片,导致内存空间的利用率降低。JVM:垃圾回收_第3张图片

  2. 复制算法(Copying):该算法将内存空间分为两个区域,每次只使用其中一个区域。当这个区域用完后,将其中的存活对象复制到另一个区域中,然后清除原来的区域。该算法的优点是不会产生内存碎片,但是需要额外的空间来存储复制对象,空间利用率较低。

  3. 标记-压缩算法(Mark and Compact):该算法在标记阶段和标记-清除算法类似,但是在清除阶段,它会将所有存活对象移动到内存的一端,然后清除另一端的所有对象。该算法的缺点是需要移动对象,因此效率较低。

在JVM当中,GC围绕着这三种算法的排列组合,结合着对于内存的分代或分区的管理方式,到现在为止,JVM一共产生了十种垃圾回收器

JVM:垃圾回收_第4张图片

分代

将内存一分为二,一部分是新生代,一部分是老年代。新创建的对象会被分配到年轻代,而长时间存活的对象则会被移动到老年代。年轻代使用复制算法,而老年代使用标记-整理或标记-压缩算法。分代可以进一步提高垃圾回收的效率,因为不同代的对象具有不同的生命周期和特性,可以针对性地选择合适的垃圾回收算法和参数。

JVM:垃圾回收_第5张图片

分区

分区模型是另一种内存管理方式,它将内存划分为多个固定大小的区域,每个区域可以分配给不同的进程或线程使用。

JVM:垃圾回收_第6张图片

如图,分区是把内存分为一个个小块。

在JVM的内存管理中,JDK8及以前采用的都是分代模型,JDK9采用的默认垃圾回收器G1首次采用了分区模型。

新生代和老年代

在分代模型中,新生代中的对象通常具有较短的生命周期,而老年代中的对象则具有较长的生命周期。当新生代中的对象经过多次垃圾回收仍然存活时,它们就会被转移到老年代中。

具体来说,在新生代中,通常将内存分为一个Eden空间和两个Survivor空间。当Eden空间满时,会触发一次垃圾回收,将存活的对象移动到其中一个Survivor空间中。当这个Survivor空间也满时,会将存活的对象移动到另一个Survivor空间中。当第二个Survivor空间也满时,会将存活的对象移动到老年代中。

因此,新生代中的对象需要经过多次垃圾回收才能转移到老年代中,而且只有在对象的生命周期比较长或者内存空间不足时,才会触发这种转移。如果新生代中的对象可以在垃圾回收后被释放,那么它们就不会被转移到老年代中。

常用垃圾回收器

JVM:垃圾回收_第7张图片 JVM十种垃圾回收器
​​​​​​左边六个都基于分代模型,右边三个基于分区模型

 左边六种回收器中,连虚线的可以配合使用,上面三个用于年轻代,下面三个用于老年代

Serial

Serial回收器是Java虚拟机中的一种垃圾回收器,它是最早也是最简单的一种垃圾回收器。Serial回收器主要用于单线程环境下的垃圾回收。

假设你,你女友,男友是三个线程,你们在房间里玩耍制造了不少的垃圾,屋里没地方了,这时候你妈妈(Serial)走了进来,说来你们哥儿仨靠墙角站好(STW:stop the world,业务线程完全停止),等我收拾完了再继续。

JVM:垃圾回收_第8张图片

在JDK1.0版本,内存只有16M,这种方法还是很高效的,但是随着版本更新,内存越来越大,回收,业务停止的时间会非常长。这就是卡顿的来源。

Parallel Scavenge

Parallel Scavenge是一种并行垃圾回收器,适用于多核CPU环境下的垃圾回收。

JVM:垃圾回收_第9张图片

 随着JVM内存的增大,如果线程数过多,大量的资源被用于上下文切换,效率反而会变低。

CMS

CMS(Concurrent Mark Sweep)主要用于处理大型堆内存的垃圾回收。相比于传统的标记-清除算法,CMS算法具有更低的停顿时间,可以在不影响应用程序正常运行的情况下进行垃圾回收。

JVM:垃圾回收_第10张图片

这就好比你和男友女友在屋里玩耍,这时你爸爸妈妈爷爷奶奶全都进来,你们仨一边玩他们一边清理。

CMS算法由JDK1.4诞生,至今还在被广泛使用,但CMS的缺点是需要实时跟踪程序的运行过程来判断哪些是垃圾,会占用一定的CPU资源。

三色标记算法

三色标记算法(Tricolor Marking Algorithm)是一种用于垃圾回收的算法,它是基于可达性分析的算法之一。该算法将对象分为三个颜色:白色、灰色和黑色,用于表示对象的可达性状态。

我们首先要理解:每一个垃圾回收线程都只是工作一小段时间,其他线程在它的基础上继续工作,扫过的对象不会再扫第二遍。

在三色标记算法中,初始时所有对象都被标记为白色,表示它们尚未被访问过。然后,从根对象开始,将其标记为灰色,表示它们正在被访问。接着,遍历灰色对象的引用,将其指向的对象标记为灰色,然后将灰色对象标记为黑色,表示它们已经被访问过。最后,所有剩余的白色对象都被认为是不可达的垃圾对象,可以被回收。

特殊情况

在回收线程扫描的同时,业务线程还在不停的给对象添加逻辑,就可能会出现没有遍历到的结点。所以说在第一遍垃圾回收时很有可能会有遗漏,但是问题不大,第二遍垃圾回收就可以给清掉了

JVM:垃圾回收_第11张图片

如果在B与D断开的同时A与D产生了引用,此时由于A已经被标记为黑色,GC不会再扫描A,但此时D仍为白色,会被误清理掉。这种漏标问题的解决方案有两种

第一种是CMS的解决方案:Incremental Update 

这种方案指的是,在整个程序的运行过程中,如果发现了一个被标记为黑色的对象的成员变量指向了白色对象,那么就把这个黑色变成灰色。这个操作由JVM自己完成。

JVM:垃圾回收_第12张图片

 这个CMS的解决方案看似完美,但是隐藏着一个严重的bug,这个bug导致CMS在JDK的任何一个版本都没有被设置为默认的垃圾回收器,我们来看看CMS的这个缺陷

CMS解决方案的缺陷

JVM:垃圾回收_第13张图片

如图,假设现在A,B,D是白色,有一个垃圾回收线程m1正在标记A的时候,它标记A的时候已经标完A的第一个属性了,第二个属性还没标完 ,此时A的颜色是灰色,然后此时A把属性1指向了一个白色对象D。此时业务线程运行,A.1指向了B,B.1指向了null。我们的回收线程m1此时回到A,站在m1的角度,A.1已经标完了,再扫描完A.2之后,A就会被标为黑色,此时D被漏标。

要解决这个问题,CMS的remark阶段,也就是开始清理之前,必须从头再扫描一遍(此时需要STW)。

第二种是G1方案:SATB Snapshot At the Beginning

JVM:垃圾回收_第14张图片

在B->D消失的时候,把该引用推到GC的堆栈。只要灰色对象指向白色对象的引用消失,就把该引用推到堆栈当中。当垃圾回收线程再次回来的时候,首先要在堆栈当中找,找出该白色对象看看此时是否有对象引用D (白色),如果没有,认定为垃圾,如果有,去扫描。

那么G1如何找到谁引用了D呢,这就归功于G1的一个内部处理机制:Rset

JVM:垃圾回收_第15张图片

Rset

在G1中,堆被分为多个大小相等的区域(Region),每个区域可以是Eden、Survivor或Old区域。当一个对象从一个区域引用另一个区域的对象时,就会产生跨区域引用。

为了快速地进行垃圾回收,G1需要知道哪些区域之间存在跨区域引用。这个信息被记录在每个区域的RSet中。RSet是一个位图,它记录了当前区域中的对象引用了哪些其他区域的对象。当G1进行垃圾回收时,它会扫描每个区域的RSet,找到所有的跨区域引用,并将这些引用标记为灰色。 

JVM:垃圾回收_第16张图片每个区间头部(大概10%~15%)有一张表格记录了有哪些区间的对象记录了这个区间的对象

实战操作

首先我们打开cmd,运行指令:java -XX:+PrintCommandLineFlags -version

这是一个Java命令,用于查看Java虚拟机的命令行参数和版本信息。该命令会输出Java虚拟机的命令行参数以及Java版本信息,包括Java版本号、Java虚拟机版本号、Java虚拟机的厂商信息等。

其中,java表示要执行的Java程序,-XX:+PrintCommandLineFlags表示要输出Java虚拟机的命令行参数,-version表示要输出Java版本信息。 

JVM:垃圾回收_第17张图片

InitialHeapSize:最小堆大小,MaxHeapSize:最大堆大小,UseCompressedPointers:使用压缩类指针,UseCompressedOops:使用压缩普通对象指针,UseG1GC:使用G1垃圾回收器。 

GC常用参数:

以下是一些常用的Java虚拟机的垃圾回收参数:

  1. -Xms:初始堆大小,指定Java虚拟机启动时堆的最小值。
  2. -Xmx:最大堆大小,指定Java虚拟机堆的最大值。
  3. -XX:NewSize:新生代大小,指定新生代的大小。
  4. -XX:MaxNewSize:最大新生代大小,指定新生代的最大大小。
  5. -XX:SurvivorRatio:新生代中Eden区域和Survivor区域的比例。
  6. -XX:MaxTenuringThreshold:对象晋升老年代的年龄。
  7. -XX:PermSize:永久代大小,指定永久代的大小。
  8. -XX:MaxPermSize:最大永久代大小,指定永久代的最大大小。
  9. -XX:+UseParallelGC:使用并行垃圾回收器。
  10. -XX:+UseConcMarkSweepGC:使用CMS垃圾回收器。
  11. -XX:+UseG1GC:使用G1垃圾回收器。
  12. -XX:+PrintGC:输出GC日志。
  13. -XX:+PrintGCDetails:输出详细的GC日志。
  14. -XX:+PrintGCDateStamps:输出GC日志的时间戳。
  15. -XX:+PrintHeapAtGC:在GC前后输出堆的使用情况。

G1常用参数

下面是一些G1垃圾回收器的常用参数:

  • -XX:+UseG1GC:启用G1垃圾回收器。
  • -XX:G1HeapRegionSize=n:设置堆区域的大小,n的值为2的幂,范围为1MB到32MB,默认值为1MB。
  • -XX:MaxGCPauseMillis=n:设置最大垃圾回收停顿时间,单位为毫秒,默认值为200毫秒。
  • -XX:G1NewSizePercent=n:设置新生代大小占整个堆大小的百分比,默认值为5%。
  • -XX:G1MaxNewSizePercent=n:设置新生代最大大小占整个堆大小的百分比,默认值为60%。
  • -XX:ParallelGCThreads=n:设置并行垃圾回收线程数,默认值为与CPU核心数相同。

除了上述常用参数,G1垃圾回收器还有很多其他的参数可以配置,具体可以查看官方文档。

注意:G1不要指定年轻代大小,这是G1动态分配的。

你可能感兴趣的:(JVM,jvm,java,算法)