如果对C++这门语言熟悉的人,再来看Java,就会发现这两者对垃圾(内存)回收的策略有很大的不同。

  C++:垃圾回收很重要,我们必须要自己来回收!!!

  Java:垃圾回收很重要,我们必须交给系统来帮我们完成!!!

  我想这也能看出这两门语言设计者的心态吧,总之,Java和C++之间有一堵由内存动态分布和垃圾回收技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。

  本篇博客我们就来详细介绍Java的垃圾回收策略。

1、为什么要进行垃圾回收

  我们知道Java是一门面向对象的语言,在一个系统运行中,会伴随着很多对象的创建,而这些对象一旦创建了就占据了一定的内存,在上一篇博客Java运行时内存结构中,我们介绍过创建的对象是保存在堆中的,当对象使用完毕之后,不对其进行清理,那么会一直占据内存空间,很明显内存空间是有限的,如果不回收这些无用的对象占据的内存,那么新创建的对象申请不了内存空间,系统就会抛出异常而无法运行,所以必须要经常进行内存的回收,也就是垃圾收集。

2、为什么要了解垃圾回收

  文章开头,我们就说Java的垃圾回收是系统自动进行的,不需要我们程序员手动处理,那么我们为什么还要了解垃圾回收呢,?

  其实这也是一个程序员进阶的过程,生产项目在运行过程中,很可能会存在内存溢出、内存泄露等问题,出现了这些问题,我们应该怎么排查?以及在生产服务器有限的资源上如何更好的分配Java运行时内存区域,提高系统运行效率等,我们必须知其然知其所以然。

  PS:本篇博客只是介绍Java垃圾回收机制,关于排查内存泄漏、溢出,运行时内存区域参数调优等会在后面进行介绍。

3、回收哪部分区域内存

  还是结合上一篇博客Java运行时内存结构,我们介绍了Java运行时的内存结构,其中程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作,这几个区域的内存分配和回收都具备确定性,在方法结束或线程结束时,内存也就跟着回收了,所以不需要我们考虑。

  那么现在就剩下Java堆方法区了,这两块区域在编译期间我们并不能完全确定创建多少个对象,有些是在运行时期创建的对象,所以Java内存回收机制主要是作用在这两块区域。

4、如何判断对象为垃圾对象

  通过上面介绍了,我们了解了为什么要进行垃圾回收以及回收哪部分的垃圾,那么接下来我们怎么去区分哪些对象为垃圾呢?

  换句话来说,我们如何判断哪些对象还“活着”,哪些对象已经“死了”,那些“死了”的对象占据的内存就是我们要进行回收的。

①、引用计数算法

  这种算法是这样的:给每一个创建的对象增加一个引用计数器,每当有一个地方引用它时,这个计数器就加1;而当引用失效时,这个计数器就减1。当这个引用计数器值为0时,也就是说这个对象没有任何地方在使用它了,那么这就是一个无效的对象,便可以进行垃圾回收了。

  这种算法实现简单,而且效率也很高。但是Java没有采用该算法来进行垃圾回收,因为这种算法无法解决对象之间的循环引用问题。

  下面我们就来构造一个循环引用的例子:

  首先,有一个 Person 类,这个类有两个自引用属性,分别表示其父亲,儿子。

 package com.ys.algorithmproject.leetcode.demo.JVM;

 /**
* Create by YSOcean
*/
public class Person { private Byte[] _1MB = null; public Person() {
/**
* 这个成员属性的作用纯粹就是占据一定内存,以便在日志中查看是否被回收
*/
_1MB = new Byte[1*1024*1024];
} private Person father;
private Person son; public Person getFather() {
return father;
} public void setFather(Person father) {
this.father = father;
} public Person getSon() {
return son;
} public void setSon(Person son) {
this.son = son;
}
}

  接着,我们通过Person类构造两个对象,分别是父亲,儿子,如下:

 public static void main(String[] args) {

     Person father = new Person();
Person son = new Person();
father.setSon(son);
son.setFather(father); father = null;
son = null; /**
* 调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,
* 而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
*/
System.gc();
}

  首先,从第3-6行代码,其运行时内存结构图如下:

  

  father对象和son对象,其引用计数第一个是栈内存指向,第二个就是其属性互相引用对方,所有引用计数器都是2。

  接着我们看第8,9行代码,分别将这两个对象置为null,也就是去掉了栈内存指向。

  

  这时候其实这两个对象只是自己互相引用了,没有别的地方在引用它们,引用计数器为1,那么这两个对象按照引用计数算法实现的虚拟机就不会回收,可想而知,这是我们不能接受的。

  所以Java虚拟机都没有使用该算法来判断对象是否存活,我们可以通过增加打印虚拟机参数来验证。

  我们将上面的man函数,增加如下Java虚拟机参数,用来打印gc信息。

-verbose:gc

  在IDEA编辑器中,添加方式如下:

  

  运行结果如下:

  

  我们看到12201K->1088K(125952K)的输出,表示垃圾收集GC前有12201K,回收后剩下1088K,堆的总量为125952K,回收的内存为12201K-1088K = 11113K。

  换句话说,上面的例子Java虚拟机是有进行垃圾回收的,所以,这也间接佐证了Java虚拟机并不是采用的引用计数法来判断对象是否是垃圾。

  PS:这些参数信息详解也会在后面博客进行详细介绍。

②、根搜索算法

  我们这里直接给出结论:在主流的商用程序中(Java,C#),都是使用根搜索算法(GC Roots Tracing)来判定对象是否存活。

  该算法思路:通过一系列名为“GC Roots” 的对象作为终点,当一个对象到GC Roots 之间无法通过引用到达时,那么该对象便可以进行回收了。

  

  上图Object1,Object2,Object3,Object4到GC Roots是可达的,所以不会被作为垃圾回收。

  

  上图Object1,Object2,Object3这三个对象互相引用,但是到 GC Roots不可达,所以都会被垃圾回收掉。

  那么有哪些对象可以作为 GC Roots 呢?

  在Java语言中,有如下4中对象可以作为 GC Roots:

  

  PS:红色的对象是要被当做垃圾回收的!

 1、虚拟机栈(栈帧中的本地变量表)中引用的对象
2、方法区中的静态变量属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中(JNI)(即一般说的Native方法)的引用的对象

5、如何进行垃圾回收

  垃圾回收涉及到大量的程序细节,而且各个平台的虚拟机操作内存的方式也不一样,但是他们进行垃圾回收的算法是通用的,所以这里我们也只介绍几种通用算法。

①、标记-清除算法

  算法实现:分为标记-清除两个阶段,首先根据上面的根搜索算法标记出所有需要回收的对象,在标记完成后,然后在统一回收掉所有被标记的对象。

  缺点

  1、效率低:标记和清除这两个过程的效率都不高。

  2、容易产生内存碎片:因为内存的申请通常不是连续的,那么清除一些对象后,那么就会产生大量不连续的内存碎片,而碎片太多时,当有个大对象需要分配内存时,便会造成没有足够的连续内存分配而提前触发垃圾回收,甚至直接抛出OutOfMemoryExecption。

  

②、复制算法

  为了解决标记-清除算法的两个缺点,复制算法诞生了。

  算法实现:将可用内存按容量划分为大小相等的两块区域,每次只使用其中一块,当这一块的内存用完了,就将还活着的对象复制到另一块区域上,然后再把已使用过的内存空间一次性清理掉。

  优点:每次都是只对其中一块内存进行回收,不用考虑内存碎片的问题,而且分配内存时,只需要移动堆顶指针,按顺序进行分配即可,简单高效。

  缺点:将内存分为两块,但是每次只能使用一块,也就是说,机器的一半内存是闲置的,这资源浪费有点严重。并且如果对象存活率较高,每次都需要复制大量的对象,效率也会变得很低。

  

③、标记-整理算法

  上面我们说过复制算法会浪费一半的内存,并且对象存活率较高时,会有过多的复制操作,效率低下。

  如果对象存活率很高,基本上不会进行垃圾回收时,标记-整理算法诞生了。

  算法实现:首先标记出所有存活的对象,然后让所有存活对象向一端进行移动,最后直接清理到端边界以外的内存。

  局限性:只有对象存活率很高的情况下,使用该算法才会效率较高。

  

④、分代收集算法

  当前商业虚拟机都是采用此算法,但是其实这不是什么新的算法,而是上面几种算法的合集。

  算法实现:根据对象的存活周期不同将内存分为几块,然后不同的区域采用不同的回收算法。

    1、对于存活周期较短,每次都有大批对象死亡,只有少量存活的区域,采用复制算法,因为只需要付出少量存活对象的复制成本即可完成收集;

    2、对于存活周期较长,没有额外空间进行分配担保的区域,采用标记-整理算法,或者标记-清除算法。

  比如,对于 HotSpot 虚拟机,它将堆空间分为如下两块区域:

  

  堆有新生代和老年代两块区域组成,而新生代区域又分为三个部分,分别是 Eden,From Surivor,To Survivor ,比例是8:1:1。

  新生代采用复制算法,每次使用一块Eden区和一块Survivor区,当进行垃圾回收时,将Eden和一块Survivor区域的所有存活对象复制到另一块Survivor区域,然后清理到刚存放对象的区域,依次循环。

  老年代采用标记-清除或者标记-整理算法,根据使用的垃圾回收器来进行判断。

  至于为什么要这样,这是由于内存分配的机制导致的,新生代存的基本上都是朝生夕死的对象,而老年代存放的都是存活率很高的对象。关于内存分配下篇博客我们会详细进行介绍。

6、何时进行垃圾回收

  理清了什么是垃圾,怎么回收垃圾,最后一点就是Java虚拟机何时进行垃圾回收呢?

  程序员可以调用 System.gc()方法,手动回收,但是调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。

  其次虚拟机会自行根据当前内存大小,判断何时进行垃圾回收,比如前面所说的,新生代满了,新产生的对象无法分配内存时,便会触发垃圾回收机制。

  这里需要说明的是宣告一个对象死亡,至少要经历两次标记,前面我们说过,如果对象与GC Roots 不可达,那么此对象会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆盖 finalize()方法,或者该方法已经执行了一次,那么虚拟机都将视为没有必要执行finalize()方法。

  如果这个对象有必要执行 finalize() 方法,那么该对象将会被放置在一个有虚拟机自动建立、低优先级,名为 F-Queue 队列中,GC会对F-Queue进行第二次标记,如果对象在finalize() 方法中成功拯救了自己(比如重新与GC Roots建立连接),那么第二次标记时,就会将该对象移除即将回收的集合,否则就会被回收。

Java虚拟机详解(三)------垃圾回收的更多相关文章

  1. Java虚拟机详解----JVM常见问题总结

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...

  2. Java虚拟机内存模型及垃圾回收监控调优

    Java虚拟机内存模型及垃圾回收监控调优 如果你想理解Java垃圾回收如果工作,那么理解JVM的内存模型就显的非常重要.今天我们就来看看JVM内存的各不同部分及如果监控和实现垃圾回收调优. JVM内存 ...

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

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

  4. Java虚拟机详解04----GC算法和种类【重要】

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...

  5. Java虚拟机详解04----GC算法和种类

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...

  6. Java 虚拟机详解

    深入理解JVM 1   Java技术与Java虚拟机 说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言.Java类文件格式.Java虚 ...

  7. java虚拟机详解

    注: 此篇文章可以算是读<深入理解Java虚拟机:JVM高级特性与最佳实践>一书后的笔记总结加上我个人的心得看法. 整体总结顺序沿用了书中顺序,但多处章节用自己的话或直白或扩展的进行了重新 ...

  8. Java虚拟机详解(五)------JVM参数(持续更新)

    JVM参数有很多,其实我们直接使用默认的JVM参数,不去修改都可以满足大多数情况.但是如果你想在有限的硬件资源下,部署的系统达到最大的运行效率,那么进行相关的JVM参数设置是必不可少的.下面我们就来对 ...

  9. Java虚拟机详解02----JVM内存结构

    主要内容如下: JVM启动流程 JVM基本结构 内存模型 编译和解释运行的概念 一.JVM启动流程: JVM启动时,是由java命令/javaw命令来启动的. 二.JVM基本结构: JVM基本结构图: ...

随机推荐

  1. delphi的拖拽功能实现

    惭愧,编了这么多年程序,还没用过拖拽功能 这次同事要实现图标互换的功能,让我帮忙看一下,于是趁机研究了一下拖拽事件,发现还是比较简单的 参考了http://topic.csdn.net/u/20081 ...

  2. WCF调试日志

    WCF调试,打不了断点or远程调试时,在配置文件的<configuration>结点下面加一段,就可以在对应位置查看服务器调试日志了,远程调试完毕发送亦可! <system.diag ...

  3. Microsoft Enterprise Library 5.0 系列(四)

    企业库日志应用程序模块工作原理图: 从上图我们可以看清楚企业库日志应用程序模块的工作原理,其中LogFilter,Trace Source,Trace Listener,Log Formatter的信 ...

  4. Lucene Index Search

    转发自:  https://my.oschina.net/u/3777556/blog/1647031 什么是Lucene?? Lucene 是 apache 软件基金会发布的一个开放源代码的全文检索 ...

  5. C++的 RTTI 观念和用途(非常详细)

    自从1993年Bjarne Stroustrup [注1 ]提出有关C++ 的RTTI功能之建议﹐以及C++的异常处理(exception handling)需要RTTI:最近新推出的C++ 或多或少 ...

  6. x64内联汇编调用API(需intel编译器,vc不支持x64内联汇编)

    #include "stdafx.h" #include <windows.h> STARTUPINFOW StartInfo  = {0}; PROCESS_INFO ...

  7. c# HttpWebRequest https的一些处理

    先看下请求方法 public string Get_Request( string strUrl, CookieContainer _cookie = null, string strHost = & ...

  8. C++ Builder 控件的卸载

    控件卸载: 1.选择   BCB   菜单   File→Close   All   (关闭所有文件)     选择BCB   菜单:   Project→Options→Packages   在   ...

  9. 查看哪些redis命令拖慢了redis

    Redis提供了一个下面这样的命令统计工具: 127.0.0.1:6379> INFO commandstats # Commandstatscmdstat_get:calls=11352126 ...

  10. Md2All:好用的markdown文件转换工具,文章迁移微信公众号的利器

    目录 简介 使用体验 极速上手 更多功能 总结 简介 markdown以简单的语法和强大的功能,征服了无数技术创作者,几乎主流的技术博客网站都开始支持markdown语言撰写博客.但是微信公众号的文章 ...