前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM的内存区域划分以及垃圾回收机制详解

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

作者头像
lizelu
发布2018-01-11 11:22:12
1K0
发布2018-01-11 11:22:12
举报
文章被收录于专栏:青玉伏案青玉伏案

在我们写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的引用都是来自堆区的引用,所以是要被回收的对象。

找到了活对象后,接下来要做的就是将活对象进行复制,将其复制到堆2区。当然,复制到堆2区的对象间的内存地址是连续的,如果要分配新的内存空间的话,直接从堆空闲的一段分配即可。这样在分配内存空间时的效率是比较高的。对象复制后,要修改来自“非堆区”的引用地址。如下所示。

复制完毕后,我们直接将堆2区的中的所有内存空间进行回收即可,下方就是复制回收后的最终结果。下方的堆1区清空后,可以接收复制过来的对象了。当对堆2区进行垃圾回收时,会把堆2区的活对象拷贝到堆1区上。

从该实例中我们可以看出当内存垃圾特别多的时候“复制式”垃圾回收的效率还是比较高的,因为复制的对象比较少,清除时直接将旧的堆空间进行清理即可。但是,当垃圾比较少的时候,这种方式会复制大量的活对象,效率还是比较低的。这种方式也会将堆的存储空间进行分半。也就是说,总有一半是空闲的,堆空间的利用率不高。

3、标记-压缩回收算法

从上述“复制式”垃圾回收过程中,我们知道,垃圾多时其效率比较高,而垃圾少时,其工作方式效率是比较低的。那么,接下来,我们来介绍另一种标记-压缩回收算法,这种算法在垃圾少时的工作效率比较高,而垃圾多的情况下,工作效率反而不高,这就与“复制式”形成了互补。下方我们将会对标记-压缩回收算法进行介绍。

标记-压缩的第一部就是标记,需要将堆区中的“活对象”进行标记。上面的内容我们已经聊了什么是“活对象”,在此就不做过多赘述了。由“活对象”的特征我们可以看出,下方的活对象是内存区域1和3,所以我们将其进行标记。

标记完成后,我们就开始进行压缩了,将活对象压缩到“堆区”的一段,然后将剩余的部分进行清除。下方就是将1和3这两个活对象进行了压缩。压缩后,将下方的空间进行Clean。也就是说Clean的部分,就可以分配新的对象了。

下方截图是标记-压缩清理后的状态。标记-压缩式垃圾回收可充分利用堆区的空间,当垃圾比较少时,这种处理方式效率还是比较高的,如果垃圾太多碎片化严重时,移动的“活对象”较多,效率比较低。这种方式可以与“复制式”结合使用,根据当前堆区的垃圾状态来选择哪种回收方式。正好与“复制式”形成优势互补。将“复制式”、“标记-压缩式”的回收方式进行整合的算法,就是“分代式”垃圾回收机制,下方会详细介绍到。

4、分代式垃圾回收

“分代”即根据对象易产生垃圾的状态或者对象的大小将其分为不同的代,可分为“年轻代”、“年老代”和“永久代”。“永久代”不在堆中,再次先不做讨论。根据分代垃圾回收的特点,画了下方的简图。

在堆中,主要把区域分为“年轻代”、“年老代”。位于“年轻代”的对象内存创建的时间不长,更新比较快,易产生“内存垃圾”,所以“年轻代”的垃圾回收使用“复制式”回收方式效率比较高。“年轻代”又可分为两个区,一个是Eden Space(伊甸园)和Survivor Sprace(幸存者区)。Eden Space去主要存放那些初次被创建的对象,而Survivor Sprace存放的是从Eden Space幸存下来的“活对象”。在Survivor Sprace(幸存者区)中又分为form和to两块,用于相互复制对象来进行垃圾清理。

而“年老代”中存放的是一些“大对象”以及从Survivor Sprace中存活下来的“对象”,一般到“年老代”的对象比较稳定,产生垃圾较少,针对这种情况,使用“标记-压缩”式回收效率比较高。“分代垃圾回收”主要是分而治之,根据不同对象的特点将其分类,根据分类的特点来具体选择合适的垃圾回收方案。 

三、分代式垃圾回收的具体工作原理

当然在JVM具体的垃圾回收时,根据线程分可分为使用单个线程回收的“串行垃圾回收”,使用多个线程回收的“并行垃圾回收”。根据程序的挂起状态,又可分为“独占式回收”和“并发式回收”。当然之前也多次聊过“并行”与“并发”绝对不是一个概念,切不可将其混淆。本篇博客就不对上述这些方式进行详述了,感兴趣的,请自行Google。

下面我们来看一下“分代式垃圾回收”的具体工作原理的完整步骤,来直观的感受一下“分代式”的垃圾回收的执行方式。

1、垃圾回收前

下图是等待“分代垃圾回收”的简图,从下图中,我们可以看出在堆中有些已分配的对象内存并没有被栈上引用,这些就是要被回收的对象。我们可以看出,下方的堆,整体上分为“年轻代”和“年老代”,而年轻代,有可细分为Eden Space, From以及To三个区域。关于每个区域的作用,在上面介绍“分代垃圾回收”时,我们已经介绍过了,所以在此部分我们不做详细介绍了。

2、分代垃圾回收

下图是对上述堆控件的垃圾回收过程。因为我们有上图可以看出,To区域是空白区,可以接受被复制的对象。由于“年轻代”易产生内存垃圾,所以采用“复制式”内存回收的方式。我们将Eden Space和From两个堆区块中的“活对象”拷贝到To区。拷贝的同时,我们也要修改被拷贝内存的栈引用地址。而对From或者Eden区域的“大对象”存储空间直接将其复制到“年老代”。因为“大对象”在From与To区多次复制的效率比较低,直接将其加入到“年老代”中以提高回收效率。

对于“年老代”的垃圾回收,就采用“标记-压缩”式垃圾回收。首先,先将活对象进行“标记”。

3、垃圾回收后的结果

下方就是“分代”垃圾回收后的具体结果。从下方简图中,我们可以看出,Eden Space和From中的活对象都被复制到了To区,而“年老代”的堆区的存储空间也变化不少。而且在“年老代”中多出了从From区复制过来的大对象。具体如下所示。

四、Eclipse的GC日志配置与分析

上面聊这么多,接下来我们来直观的感受一下在Eclipse如何查看垃圾回收的过程以及分析垃圾回收的日志信息。默认情况下,是不显示垃圾回收的过程以及打印日志的,需要在运行配置中添加相关的配置项来将垃圾回收的日志进行打印。本部分我们来看一下Eclipse中的垃圾回收日志记录的配置,然后我们来分析一下这些日志记录。当然我们本篇博客中使用的是Java8,如果你用其他版本的Java打印出来的日志信息会略有不同,好开始本部分的内容。

1、配置Eclipse的运行设置

在Eclipse中的运行设置中添加相应的配置项,垃圾回收时才会打印相应的日志信息。选择我们的工程,然后找到Run Configurations…选项,进行运行时的配置。

下方就是上述选项打开的对话框,然后找到(x)=Arguments这个标签栏,在VM arguments中添加相应的虚拟机参数,这些参数都会作为工程在运行时的参数。下方我们添加了-XX:+PrintGCTimeStamps-XX:+PrintGCDetails两个参数。由这两个参数名我们不难看出相应参数所对应的功能,一个是打印垃圾回收时的时间戳,另一个是打印垃圾回收时的细节。当然还有好多其他的参数,比如选择“垃圾回收”时的具体算法的参数,以及选择是“串行”还是“并行”的参数,还有一些选择是“独占式”还是“并发式”垃圾回收的参数。在此就不做过多赘述了,请自行Google。

2、回收日志的打印与解析

配置完上述参数后,当我们使用System.gc(); 来进行强制垃圾回收时,会打印出相应的参数信息。首先我们得创建测试用的代码,下方就是我们所创建的测试类,当然测试类中的代码比较简单。主要就是new了以字符串,然后将引用置为null, 最后调用System.gc()进行回收。具体代码如下所示:

代码语言:javascript
复制
package com.zeluli.gclog;

public class GCLogTest {
    public static void main(String[] args) {
        String s = new String("Value");
        s = null;
        System.gc();
    }
}

下方就是上述代码所运行的效果,接下来我们将对下方日志信息的主要内容进行介绍。

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco }

  • [PSYoungGen: 1997K->416K(38400K)] 1997K->424K(125952K), 0.0010277 secs]
    • PSYoungGen表示,并行对“年轻代”进行回收,1997K->416K表示年轻代相应区域中“回收前->回收后”的大小,而(38400K)表示“年轻代”堆的总大小。而后方的1997K->424K(125952K)数据是以整个堆的角度来看待的问题。1997K(堆回收前使用的内存) -> 424K(堆回收后使用的内存)(125952K-堆的总内存空间)。

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco }

  • [ParOldGen: 8K->328K(87552K)]
    • ParOldGen并行回收“年老代”,后边的参数与上述并行回收年轻代的参数类似,就不多说了。
  • [Metaspace: 2669K->2669K(1056768K)]
    • 则表示“元数据区”的回收情况,Metaspace及“永久代”区,用于存放静态数据或者系统方法的区域。  

上述就是简单的垃圾回收的日志,本篇博客的内容就先到这儿吧,关于JVM中的垃圾回收的内容还有好多,以后结合着具体情况,再陆陆续续的进行介绍。今天博客就先到这儿。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-06-02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档