JVM的内存区域划分以及垃圾回收机制详解 - 青玉

      2020-09-17 08:11      HTML5

在我们写Java代码时,大部分情况下是不用关心你New的对象是否被释放掉,或者什么时候被释放掉。因为JVM中有垃圾自动回收机制。在之前的博客中我们聊过Objective-C中的MRC(手动引用计数)以及ARC(自动引用计数)的内存管理方式,下方会对其进行回顾。而目前的JVM的内存回收机制则不是使用的引用计数,而是主要使用的“复制式回收”和“自适应回收”。

当然除了上面是这两种算法外,还有其他是算法,下方也将会对其进行介绍。本篇博客,我们先简单聊一下JVM的区域划分,然后在此基础上介绍一下JVM的垃圾回收机制。

一、JVM内存区域划分简述

当然本部分简单的聊一下JVM的内存区域的划分,为下方垃圾回收机制内容的展开进行铺垫。当然对JVM内存区域划分的内容网上有好多详细的内容,请自行Google。

根据JVM内存区域的划分,简单的画了下方的这个示意图。区域主要分为两大块,一块是堆区(Heap),我们所New出的对象都会在堆区进行分配,在C语言中的malloc所分配的方法就是从Heap区获取的。而垃圾回收器主要是对堆区的内存进行回收的。

而另一部分则是非堆区,非堆区主要包括用于编译和保存本地代码的“代码缓存区(Code Cache)”、保存JVM自己的静态数据的“永生代(Perm Gen)”、存放方法参数局部变量等引用以及记录方法调用顺序的“Java虚拟机栈(JVM Stack)”和“本地方法栈(Local Method Stack)”。

  

垃圾回收器主要回收的是堆区中未使用的内存区域,并对相应的区域进行整理。在堆区中,又根据对象内存的存活时间或者对象大小,分为“年轻代”和“年老代”。“年轻代”中的对象是不稳定的易产生垃圾,而“年老代”中的对象比较稳定,不易产生垃圾。之所以将其分开,是分而治之,根据不同区域的内存块的特点,采取不同的内存回收算法,从而提高堆区的垃圾回收的效率。下方会给出具体的介绍。

二、常见的内存回收算法简介

上面我们简单的了解的JVM中内存区域的划分,接下来我们就来看一下几种常见的内存回收算法。当然,下方所介绍的内存回收的算法不仅仅是JVM中所使用到的,我们还会回顾一下OC中的内存回收方式。下方主要包括“引用计数式回收”、“复制式回收”、“标记整理式回收”、“分代式回收”。

1、引用计数式内存回收

引用计数(Reference Count)式内存回收机制是Objective-C以及Swift语言中正在使用的内存回收机制,在之前的博客中我们也详细的聊过引用计数式的内存回收。只要有引用,那么引用计数就加1。当引用计数为0时,该块内存就会被回收。当然这中内存清理方式容易形成“引用循环”。

在Objective-C的引用计数中循环引用而造成内存泄露的问题,可以将变量声明成weak或者strong类型。也就是说我们可以将引用定义为“强引用”或者“弱引用”。当出现“强引用循环”时,我们将其中的一个引用设置为weak类型即可,然后这种强引用循环就被打破了,也就不会造成“内存泄露”的问题。关于“引用计数式内存回收”的更多以及更详细的内容,请参考之前发布的关于OC内容的相关博客。

为了更清晰的了解引用计数的工作方式,就简单的画了下方这个图。在左边的栈中的a、b、c三个引用分别指向堆中的不同区域块。在堆中的内存区域块中,该区域有一个强引用时,其retainCount就会加1。而在弱引用时,就retainCount就不会加1。

我们先来看看a引用的第1块内存区域,因为该内存块只有a在强引用,所以retainCount=1,当a不在引用该内存区域时,retainCount=0,该内存会理解被回收的。这种情况下是不会造成内存泄露的。

我们再来看看b指向的内存区域2。b和内存块3都强引用了内存块2,所以2的retainCount=2。而内存块2也强引用了内存块3,所以3的retainCount=1。所以b指向的这块内存区域就存在“强引用循环”,因为当b不再指向这块内存区域时,rc=2就会变为rc=1。因为retainCount不为零,所以这2块内存区域是不会被释放的,2不会被释放,那么自然而然的3块内存区域也不会被释放,但是这块内存区域有不会再被使用到了,所以就会造成“内存泄露”的情况。如果这两块内存区域特别大,那么我们可想而知,后果是比较严重的。

像c引用的这块情况,就不会引起“强引用循环”,因为其中的一个引用链是是弱引用的。当c不在引用第4块内存时,rc由1变为零,那么该块区域就会被立即释放。而内存块4被释放后,内存块5的rc由1变为0,内存块5也会被释放掉。这种情况下是不会引起内存泄露的。而在Objective-C中正是采用的这种方式来回收内存的,当然了,在OC中除了“强引用”和“弱引用”外,还有自动释放池。也就是说,Autorealease类型的引用,让retainCount = 0时,不会被立即释放掉,而是在出自动释放池时才会被释放掉,在此就不做过多赘述了。

  

2、复制式内存回收

聊完引用计数回收,我们知道引用计数容易引起“循环引用”的问题,为了解决“循环引用”引起的内存泄露问题,OC中引入和“强引用”和“弱引用”的概念。接下来我们在看看复制式内存回收机制,在该机制中是不需要关心“循环引用”的问题的。简单的说,复制式回收其核心就是“复制”,但前提是有条件复制。在垃圾回收时,将“活对象”复制到另一块空白的堆区,然后将之前的区域一并清除。“活对象”就是指沿着对象的引用链可以到“栈”上的对象。当然在将活对象复制到新的“堆区”后,也要将栈区的引用进行修改。

下方就是我们画的复制式回收的简图,主要将堆分为两大部分,在进行垃圾回收时,会将一个堆上的活对象复制到另一个堆上。下方堆1区是目前正在使用的区块,堆2区则是空闲区。而在堆1区中未被标记的那些内存块,也就是2、3是要被回收的垃圾对象。而1、4、5是要被复制的“活对象”。因为沿着栈上的a可到达区块1、沿着c可到达区块4、5。而区块2和3虽然有引用,但是不是来自非堆区,也就是2和3的引用都是来自堆区的引用,所以是要被回收的对象。