1. 前言

  Java和C++之间显著的一个区别就是对内存的管理。和C++把内存管理的权利赋予给开发人员的方式不同,Java拥有一套自动的内存回收系统(Garbage Collection,GC)简称GC,可以无需开发人员干预而对不再使用的内存进行回收管理。

  垃圾回收技术(以下简称GC)是一套自动的内存管理机制。当计算机系统中的内存不再使用的时候,把这些空闲的内存空间释放出来重新投入使用,这种内存资源管理的机制就称为垃圾回收。

  其实GC并不是Java的专利,GC的的发展历史远比Java来得久远的多。早在Lisp语言中,就有GC的功能,包括其他很多语言,如:Python(其实Python的历史也比Java早)也具有垃圾回收功能。

  使用GC的好处,可以把这种容易犯错的行为让给计算机系统自己去管理,可以防止人为的错误。同时也把开发人员从内存管理的泥沼中解放出来。

  虽然使用GC虽然有很多方便之处,但是如果不了解GC机制是如何运作的,那么当遇到问题的时候,我们将会很被动。所以有必要学习下Java虚拟机中的GC机制,这样我们才可以更好的利用这项技术。当遇到问题,比如内存泄露或内存溢出的时候,或者垃圾回收操作影响系统性能的时候,我们可以快速的定位问题,解决问题。

  接下来,我们来看下JVM中的GC机制是怎么样的。

2. 哪些内存可以回收

  首先,我们如果要进行垃圾回收,那么我们必须先要识别出哪些是垃圾(被占用的无用内存资源)。

  Java虚拟机将内存划分为多个区域,分别做不同的用途。简单的将,JVM对内存划分为这几个内存区域:程序计数器、虚拟机栈、本地方法栈、Java堆和方法区。其中程序计数器、虚拟机栈和本地方法栈是随着线程的生命周期出生和死亡的,所以这三块区域的内存在程序执行过程中是会有序的自动产生和回收的,我们可以不用关心它们的回收问题。剩下的Java堆和方法区,它们是JVM中所有线程共享的区域。由于程序执行路径的不确定性,这部分的内存分配和回收是动态进行的,GC主要关注这部分的内存的回收。

  对像实例是否是存活的,有两种算法可以用于确定哪些实例是死亡的(它们占用的内存就是垃圾),那么些实例是存活的。第一种是引用计数算法:

2.1 引用计数算法

  引用计数算法会对每个对象添加一个引用计数器,每当一个对象在别的地方被引用的时候,它的引用计数器就会加1;当引用失效的时候,它的引用计数器就会减1。如果一个对象的引用计数变成了0,那么表示这个对象没有被任何其他对象引用,那么就可以认为这个对象是一个死亡的对象(它占用的内存就是垃圾),这个对象就可以被GC安全地回收而不会导致系统出现问题。

  我们可以发现,这种计数算法挺简单的。在C++中的智能指针,也是使用这种方式来跟踪对象引用的,来达到内存自动管理的。引用计数算法实现简单,而且判断高效,在大部分情况下是一个很好的垃圾标记算法。在Python中,就是采用这种方式来进行内存管理的。但是,这个算法存在一个明显的缺陷:如果两个对象之间有循环引用,那么这两个对象的引用计数将永远不会变成0,即使这两个对象没有被任何其他对象引用。

public class ReferenceCountTest {

    public Object ref = null;

    public static void main(String ...args) {
ReferenceCountTest objA = new ReferenceCountTest();
ReferenceCountTest objB = new ReferenceCountTest(); // 循环引用 objA <--> objB
objA.ref = objB;
objB.ref = objA; // 去除外部对这两个对象引用
objA = null;
objB = null; System.gc();
}
}

上面的代码就演示了两个对象之间出现循环引用的情况。这个时候objA和objB的引用计数都是1,由于两个对象之间是循环引用的,所以它们的引用计数将一直是1,而即使这两个对象已经不再被系统所使用到。

由于引用计数这种算法存在这种缺陷,所以就有了一种称为“可达性分析算法”的算法来标记垃圾对象。

2.2 可达性分析算法

  通过可达性分析算法来判断对象存活,可以克服上面提到的循环引用的问题。在很多编程语言中都采用这种算法来判断对象是否存活。

  这种算法的基本思路是,确定出一系列的称为“GC Roots”的对象,以这些对象作为起始点,向下搜索所有可达的对象。搜索过程中所走过的路径称为“引用链”。当一个对象没有被任何到“GC Roots”对象的“引用链”连接的时候,那么这个对象就是不可达的,这个对象就被认为是垃圾对象。

  从上面的图中可以看出,object1~4这4个对象,对于GC Roots这个对象来说都是可达的。而object5~7这三个对象,由于没有连接GC Roots的引用链,所以这三个对象时不可达的,被判定为垃圾对象,可以被GC回收。

  在Java中,可以作为GC Roots的对象有以下几种:

  • 虚拟机栈中的本地变量表中引用的对象,也就是正在执行函数体中的局部变量引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中(Native方法中)引用的对象  

2.3 怎么判一个对象"死刑"

  当通过可达性分析算法判定为不可达的对象,我们也不能断定这个对象就是需要被回收的。当我们需要真正回收一个对象的时候,这个对象必须经历至少两次标记过程:

  当通过可达性分析算法处理以后,这个对象没有和GC Roots相连的引用链,那么这个对象就会被第一次标记,并判断对象的finalize()方法(在Java的Object对象中,有一个finalize()方法,我们创建的对象可以选择是否重写这个方法的实现)是否需要执行,如果对象的类没有覆盖这个finalize()方法或者finalize()已经被执行过了,那么就不需要再执行一次该方法了。

  如果这个对象的finalize()方法需要被执行,那么这个对象会被放到一个称为F-Queue的队列中,这个队列会被由Java虚拟机自动创建的一个低优先级Finalizer线程去消费,去执行(虚拟机只是触发这个方法,但是不会等待方法调用返回。这么做是为了保证:如果方法执行过程中出现阻塞,性能问题或者发生了死循环,Finalizer线程仍旧可以不受影响地消费队列,不影响垃圾回收的过程)队列中的对象的finalize()方法。

  稍后,GC会对F-Queue队列中的对象进行第二次标记,如果在这次标记发生的时候,队列中的对象确实没有存活(没有和GC Roots之间有引用链),那么这个对象就确定会被系统回收了。当然,如果在队列中的对象,在进行第二次标记的时候,突然和GC Roots之间创建了引用链,那么这个对象就"救活"了自己,那么在第二次标记的时候,这个存活的对象就被移除出待回收的集合了。所以,通过这种两次标记的机制,我们可以通过在finalize()方法中想办法让对象重新和GC Roots对象建立链接,那么这个对象就可以被救活了。

  下面的代码,通过在finalize()方法中将this指针赋值给类的静态属性来"拯救"自己:

public class FinalizerTest {
private static Object HOOK_REF; public static void main(String ...args) throws Exception {
HOOK_REF = new FinalizerTest(); // 将null赋值给HOOK_REF,使得原先创建的对象变成可回收的对象
HOOK_REF = null;
System.gc();
Thread.sleep(1000); if (HOOK_REF != null) {
System.out.println("first gc, object is alive");
} else {
System.out.println("first gc, object is dead");
} // 如果对象存活了,再次执行一次上面的代码
HOOK_REF = null;
System.gc();
if (HOOK_REF != null) {
System.out.println("second gc, object is alive");
} else {
System.out.println("second gc, object is dead");
}
} @Override
protected void finalize() throws Throwable {
super.finalize();
// 在这里将this赋值给静态变量,使对象可以重新和GC Roots对象创建引用链
HOOK_REF = this;
System.out.println("execute in finalize()");
}
} #output:

  execute in finalize()
  first gc, object is alive
  second gc, object is dead

  可以看到,第一次执行System.gc()的时候,通过在方法finalize()中将this指针指向HOOK_REF来重建引用链接,使得本应该被回收的对象重新复活了。而对比同样的第二段代码,没有成功拯救的原因是:finalize()方法只会被执行一次,所以当第二次将HOOK_REF赋值为null,释放对对象的引用的时候,由于finalize()方法已经被执行过一次了,所以没法再通过finalize()方法中的代码来拯救对象了,导致对象被回收。

3. 怎么回收内存

  上面我们已经知道了怎么识别出可以回收的垃圾对象。现在,我们需要考虑如何对这些垃圾进行有效的回收。垃圾收集的算法大致可以分为三类:

  1. 标记-清除算法
  2. 标记-复制算法
  3. 标记-整理算法

  这三种算法,适用于不同的回收需求和场景。下面,我们来一一介绍下每个回收算法的思想。

3.1 标记-清除算法

  "标记-清除"算法是最基础的垃圾收集算法。标记-清除算法在执行的时候,分为两个阶段:分别是"标记"阶段和"清除"阶段。

  在标记阶段,它会根据上面提到的可达性分析算法标记出哪些对象是可以被回收的,然后在清除阶段将这些垃圾对象清理掉。

  算法思路很简单,但是这个算法存在一些缺陷:首先标记和清除这两个过程的效率不高,其次是,直接将标记的对象清除以后,会导致产生很多不连续的内存碎片,而太多不连续的碎片会导致后续分配大块内存的时候,没有连续的空间可以分配,这会导致不得不再次触发垃圾回收操作,影响性能。

  

3.2 复制算法

  复制算法,顾名思义,和复制操作有关。该算法将内存区域划分为大小相等的两块内存区域,每次只是用其中的一块区域,另一块区域闲置备用。当进行垃圾回收的时候,会将当前是用的那块内存上的存活的对象直接复制到另外一块闲置的空闲内存上,然后将之前使用的那块内存上的对象全部清理干净。

  这种处理方式的好处是,可以有效的处理在标记-清除算法中碰到的内存碎片的问题,实现简单,效率高。但是也有一个问题,由于每次只使用其中的一半内存,所以在运行时会浪费掉一半的内存空间用于复制,内存空间的使用率不高。

3.3 标记-整理算法

  标记-整理算法,思路就是先进行垃圾内存的标记,这个和标记-清除算法中的标记阶段一样。当将标记出来的垃圾对象清除以后,为了避免出现标记-清除算法中碰到的内存碎片问题,标记-整理算法会对内存区域进行整理。将当前的所有存活的对象移动到内存的一端,将一端的空闲内存整理出来,这样就可以得到一块连续的空闲内存空间了。

  这样做,可以很方便地申请新的内存,只要移动内存指针就可以划出需要的内存区域以存放新的对象,可以在不浪费内存的情况下高效的分配内存,避免了在复制算法中浪费一部分内存的问题。

4. 分代收集

  在现代虚拟机实现中,会将整块内存划分为多个区域。用"年龄"的概念来描述内存中的对象的存活时间,并将不同年龄段的对象分类存放在不同的内存区域。这样,就有了我们平时听说的"年轻代"、"老年代"等术语。

  顾名思义,"年轻代"中的对象一般都是刚出生的对象,而"老年代"中的对象,一般都是在程序运行阶段长时间存活的对象。将内存中的对象分代管理的好处是,可以按照不同年龄代的对象的特点,使用合适的垃圾收集算法。

  对于"年轻代"中的对象,由于其中的大部分对象的存活时间较短,很多对象都撑不过下一次垃圾收集,所以在年轻代中,一般都使用"复制算法"来实现垃圾收集器。

  在上图中,我们可以看到"Young Generation"标记的这块区域就是"年轻代"。在年轻代中,还细分了三块区域,分别是:"eden"、"S0"和"S1",其中"eden"是新对象出生的地方,而"S0"和"S1"就是我们在复制算法中说到了那两块相等的内存区域,称为存活区(Survivor Space)。

  这里用于复制的区域只是占用了整个年轻代的一部分,由于在新生代中的对象大部分的存活时间都很短,所以如果按照复制算法中的以1:1的方式来平分年轻代的话,会浪费很多内存空间。所以将年轻代划分为上图中所示的,一块较大的eden区和两块同等大小的survivor区,每次只使用eden区和其中的一块survivor区,当进行内存回收的时候,会将当前存活的对象一次性复制到另一块空闲的survivor区上,然后将之前使用的eden区和survivor区清理干净,现在,年轻代可以使用的内存就变成eden区和之前存放存活对象的那个survivor区了,S0和S1这两块区域是轮替使用的。

  HotSpot虚拟机默认Eden区和其中一块Survivor区的占比是8:1,通过JVM参数"-XX:SurvivorRatio"控制这个比值。SurvivorRatio的值是一个整数,表示Eden区域是一块Survivor区域的大小的几倍,所以,如果SurvivorRatio的值是8,那么Eden区和其中Survivor区的占比就是8:1,那么总的年轻代的大小就是(Eden + S0 + S1) = (8 + 1 + 1) = 10,所以年轻代每次可以使用的内存空间就是(Eden + S0) = (8 + 1)  = 9,占了整个年轻代的 9 / 10 = 90%,而每次只浪费了10%的内存空间用于复制。

  并不是留出越少的空间用于复制操作越好,如果在进行垃圾收集的时候,出现大部分对象都存活的情况,那么空闲的那块很小的Survivor区域将不能存放这些存活的对象。当Survivor空间不够用的时候,如果满足条件,可以通过分配担保机制,向老年代申请内存以存放这些存活的对象。

  对于老年代的对象,由于在这块区域中的对象和年轻代的对象相比较而言存活时间都很长,在这块区域中,一般通过"标记-清理算法"和"标记-整理算法"来实现垃圾收集机制。上图中的Tenured区域就是老年代所在的区域。而最后那块Permanent区域,称之为永久代,在这块区域中,主要是存放类对象的信息、常量等信息,这个区域也称为方法区。在Java 8中,移除了永久区,使用元空间(metaspace)代替了。

5. 总结

  在这篇文章中,我们首先介绍了采用最简单的引用计数法来跟踪垃圾对象和通过可达性分析算法来跟踪垃圾对象。然后,介绍了垃圾回收中用到的三种回收算法:标记-清除、复制、标记-整理,以及它们各自的优缺点。最后,我们结合上面介绍的三种回收算法,介绍了现代JVM中采用的分代回收机制,以及不同分代采用的回收算法。

学习JVM--垃圾回收(一)原理的更多相关文章

  1. JVM学习--(四)垃圾回收算法

    我们都知道java语言与C语言最大的区别就是内存自动回收,那么JVM是怎么控制内存回收的,这篇文章将介绍JVM垃圾回收的几种算法,从而了解内存回收的基本原理. stop the world 在介绍垃圾 ...

  2. Java虚拟机学习笔记——JVM垃圾回收机制

    Java虚拟机学习笔记——JVM垃圾回收机制 Java垃圾回收基于虚拟机的自动内存管理机制,我们不需要为每一个对象进行释放内存,不容易发生内存泄漏和内存溢出问题. 但是自动内存管理机制不是万能药,我们 ...

  3. jvm垃圾回收原理(转)

    原文链接:jvm垃圾回收原理 在jvm中堆空间划分为三个代:年轻代(Young Generation).年老代(Old Generation)和永久代(Permanent Generation).年轻 ...

  4. JVM学习笔记——垃圾回收篇

    JVM学习笔记--垃圾回收篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分 我们会分为以下几部分进行介绍: 判断垃圾回收对象 垃圾回收算法 分代垃圾回收 垃圾回收器 ...

  5. JVM学习(三)JVM垃圾回收

    一.引用的分类 在了解JVM垃圾回收机制之前,了解一下对象的引用类型是非常必要的. 强引用:GC时不会被回收 软引用:描述有用但不是必须的对象,在发生内存溢出异常之前被回收 弱引用:描述有用但不是必须 ...

  6. JVM垃圾回收篇

    点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. 基础概念 GC=jvm垃圾回收,垃圾回收机制是由垃圾回收器Garbage ...

  7. JVM垃圾回收机制总结:调优方法

    转载: JVM垃圾回收机制总结:调优方法 JVM 优化经验总结 JVM 垃圾回收器工作原理及使用实例介绍

  8. JVM 垃圾回收器工作原理及使用实例介绍

    IBM介绍文档:https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/ Java 的新生代串行垃圾回收器中使用了复制 ...

  9. JVM内存管理和JVM垃圾回收机制

    JVM内存管理和JVM垃圾回收机制(1) 这里向大家描述一下JVM学习笔记之JVM内存管理和JVM垃圾回收的概念,JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,另外JVM分别对新生代和旧生代采 ...

  10. JDK分析工具&JVM垃圾回收(转)

    转自:http://blog.163.com/itjin45@126/blog/static/10510751320144201519454/ 官方手册:http://docs.oracle.com/ ...

随机推荐

  1. 构建一个基本的前端自动化开发环境 —— 基于 Gulp 的前端集成解决方案(四)

    通过前面几节的准备工作,对于 npm / node / gulp 应该已经有了基本的认识,本节主要介绍如何构建一个基本的前端自动化开发环境. 下面将逐步构建一个可以自动编译 sass 文件.压缩 ja ...

  2. bootstrap + requireJS+ director+ knockout + web API = 一个时髦的单页程序

    也许单页程序(Single Page Application)并不是什么时髦的玩意,像Gmail在很早之前就已经在使用这种模式.通常的说法是它通过避免页面刷新大大提高了网站的响应性,像操作桌面应用程序 ...

  3. .NET 基础 一步步 一幕幕[面向对象之构造函数、析构函数]

    构造函数.析构函数 构造函数: 语法: //无参的构造函数 [访问修饰符] 函数名() :函数名必须与类名相同. //有参的构造函数 [访问修饰符] 函数名(参数列表):函数名必须与类名相同. 作用: ...

  4. div实现自适应高度的textarea,实现angular双向绑定

    相信不少同学模拟过腾讯的QQ做一个聊天应用,至少我是其中一个. 过程中我遇到的一个问题就是QQ输入框,自适应高度,最高高度为3row. 如果你也像我一样打算使用textarea,那么很抱歉,你一开始就 ...

  5. 窥探Vue.js 2.0 - Virtual DOM到底是个什么鬼?

    引言 你可能听说在Vue.js 2.0已经发布,并且在其中新添加如了一些新功能.其中一个功能就是"Virtual DOM". Virtual DOM是什么 在之前,React和Em ...

  6. IL异常处理

    异常处理在程序中也算是比较重要的一部分了,IL异常处理在C#里面实现会用到一些新的方法 1.BeginExceptionBlock:异常块代码开始,相当于try,但是感觉又不太像 2.EndExcep ...

  7. JavaScript学习笔记(二)——闭包、IIFE、apply、函数与对象

    一.闭包(Closure) 1.1.闭包相关的问题 请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,第10个显示9:方法:找到所有的div, ...

  8. Register-SPWorkflowService 404

    最近需要做一个SharePoint 2013工作流演示环境. 于是在自己的本子上安装了一个虚拟机. 虚拟机操作系统是Windows Server 2012 R2,计划把AD.SQL Server 20 ...

  9. .NET面试题系列[5] - 垃圾回收:概念与策略

    面试出现频率:经常出现,但通常不会问的十分深入.通常来说,看完我这篇文章就足够应付面试了.面试时主要考察垃圾回收的基本概念,标记-压缩算法,以及对于微软的垃圾回收模板的理解.知道什么时候需要继承IDi ...

  10. HTML5- Canvas入门(五)

    今天要介绍的是canvas对图形对象的操作,包括图像.视频绘制,和操作像素对象的方法. 图片/视频的绘制 在canvas中,我们可以通过 drawImage() 的方法来绘制图片或视频文件,其语法为: ...