转载请注明出处:http://blog.csdn.net/ns_code/article/details/18076173

 

对象引用

Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例。谈到Java堆中的垃圾回收,自然要谈到引用。在JDK1.2之前,Java中的引用定义很很纯粹:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但在JDK1.2之后,Java对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。

  • 强引用:如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。
  • 弱引用:它也是用来描述非需对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存岛下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。

垃圾对象的判定

Java堆中存放着几乎所有的对象实例,垃圾收集器对堆中的对象进行回收前,要先确定这些对象是否还有用,判定对象是否为垃圾对象有如下算法:

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器都为0的对象就是不可能再被使用的。

引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择,当Java语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。

根搜索算法

Java和C#中都是采用根搜索算法来判定对象是否存活的。这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。在Java语言里,可作为GC Roots的兑现包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI(Native方法)的引用对象。

实际上,在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

垃圾收集算法

判定除了垃圾对象之后,便可以进行垃圾回收了。下面介绍一些垃圾收集算法,由于垃圾收集算法的实现涉及大量的程序细节,因此这里主要是阐明各算法的实现思想,而不去细论算法的具体实现。

标记—清除算法

标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:

回收前状态:

回收后状态:

该算法有如下缺点:

  • 标记和清除过程的效率都不高。
  • 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。

复制算法

复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲课用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:

  • 每次只对一块内存进行回收,运行高效。
  • 只需移动栈顶指针,按顺序分配内存即可,实现简单。
  • 内存回收时不用考虑内存碎片的出现。

它的缺点是:可一次性分配的最大内存缩小了一半。

复制算法的执行情况如下图所示:

回收前状态:

回收后状态:

标记—整理算法

复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:

回收前状态:

回收后状态:

分代收集

当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

垃圾收集器

垃圾收集器是内存回收算法的具体实现,Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别。Sun  HotSpot虚拟机1.6版包含了如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old。这些收集器以不同的组合形式配合工作来完成不同分代区的垃圾收集工作。

垃圾回收分析

    在用代码分析之前,我们对内存的分配策略明确以下三点:
  • 对象优先在Eden分配。
  • 大对象直接进入老年代。
  • 长期存活的对象将进入老年代。
    对垃圾回收策略说明以下两点:

  • 新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。

下面我们来看如下代码:

[java]  view plain copy
  1. public class SlotGc{
  2. public static void main(String[] args){
  3. byte[] holder = new byte[32*1024*1024];
  4. System.gc();
  5. }
  6. }

代码很简单,就是向内存中填充了32MB的数据,然后通过虚拟机进行垃圾收集。在Javac编译后,我们执行如下指令:java -verbose:gc SlotGc来查看垃圾收集的结果,得到如下输出信息:

[GC 208K->134K(5056K), 0.0017306 secs]

[Full GC 134K->134K(5056K), 0.0121194 secs]

[Full GC 32902K->32902K(37828K), 0.0094149 sec

注意第三行,“->”之前的数据表示垃圾回收前堆中存活对象所占用的内存大小,“->”之后的数据表示垃圾回收堆中存活对象所占用的内存大小,括号中的数据表示堆内存的总容量,0.0094149 sec 表示垃圾回收所用的时间。

从结果中可以看出,System.gc(()运行后并没有回收掉这32MB的内存,这应该是意料之中的结果,因为变量holder还处在作用域内,虚拟机自然不会回收掉holder引用的对象所占用的内存。

我们把代码修改如下:

[java]  view plain copy
  1. public class SlotGc{
  2. public static void main(String[] args){
  3. {
  4. byte[] holder = new byte[32*1024*1024];
  5. }
  6. System.gc();
  7. }
  8. }

加入花括号后,holder的作用域被限制在了花括号之内,因此,在执行System.gc()时,holder引用已经不能再被访问,逻辑上来讲,这次应该会回收掉holder引用的对象所占的内存。但查看垃圾回收情况时,输出信息如下:

[GC 208K->134K(5056K), 0.0017100 secs]

[Full GC 134K->134K(5056K), 0.0125887 secs]

[Full GC 32902K->32902K(37828K), 0.0089226 secs]

很明显,这32MB的数据并没有被回收。下面我们再做如下修改:

[java]  view plain copy
  1. public class SlotGc{
  2. public static void main(String[] args){
  3. {
  4. byte[] holder = new byte[32*1024*1024];
  5. holder = null;
  6. }
  7. System.gc();
  8. }
  9. }

这次得到的垃圾回收信息如下:

[GC 208K->134K(5056K), 0.0017194 secs]

[Full GC 134K->134K(5056K), 0.0124656 secs]

[Full GC 32902K->134K(37828K), 0.0091637 secs]

说明这次holder引用的对象所占的内存被回收了。我们慢慢来分析。

首先明确一点:holder能否被回收的根本原因是局部变量表中的Slot是否还存有关于holder数组对象的引用。

在第一次修改中,虽然在holder作用域之外进行回收,但是在此之后,没有对局部变量表的读写操作,holder所占用的Slot还没有被其他变量所复用(回忆Java内存区域与内存溢出一文中关于Slot的讲解),所以作为GC Roots一部分的局部变量表仍保持者对它的关联。这种关联没有被及时打断,因此GC收集器不会将holder引用的对象内存回收掉。 在第二次修改中,在GC收集器工作前,手动将holder设置为null值,就把holder所占用的局部变量表中的Slot清空了,因此,这次GC收集器工作时将holder之前引用的对象内存回收掉了。

当然,我们也可以用其他方法来将holder引用的对象内存回收掉,只要复用holder所占用的slot即可,比如在holder作用域之外执行一次读写操作。

为对象赋null值并不是控制变量回收的最好方法,以恰当的变量作用域来控制变量回收时间才是最优雅的解决办法。另外,赋null值的操作在经过虚拟机JIT编译器优化后会被消除掉,经过JIT编译后,System.gc()执行时就可以正确地回收掉内存,而无需赋null值。

性能调优

Java虚拟机的内存管理与垃圾收集是虚拟机结构体系中最重要的组成部分,对程序(尤其服务器端)的性能和稳定性有着非常重要的影响。性能调优需要具体情况具体分析,而且实际分析时可能需要考虑的方面很多,这里仅就一些简单常用的情况作简要介绍。

  • 我们可以通过给Java虚拟机分配超大堆(前提是物理机的内存足够大)来提升服务器的响应速度,但分配超大堆的前提是有把握把应用程序的Full GC频率控制得足够低,因为一次Full GC的时间造成比较长时间的停顿。控制Full GC频率的关键是保证应用中绝大多数对象的生存周期不应太长,尤其不能产生批量的、生命周期长的大对象,这样才能保证老年代的稳定。
  • Direct Memory在堆内存外分配,而且二者均受限于物理机内存,且成负相关关系,因此分配超大堆时,如果用到了NIO机制分配使用了很多的Direct Memory,则有可能导致Direct Memory的OutOfMemoryError异常,这时可以通过-XX:MaxDirectMemorySize参数调整Direct Memory的大小。
  • 除了Java堆和永久代以及直接内存外,还要注意下面这些区域也会占用较多的内存,这些内存的总和会受到操作系统进程最大内存的限制:

    1、线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError(横向无法分配,即无法建立新的线程)。

    2、Socket缓冲区:每个Socket连接都有Receive和Send两个缓冲区,分别占用大约37KB和25KB的内存。如果无法分配,可能会抛出IOException:Too many open files异常。关于Socket缓冲区的详细介绍参见我的Java网络编程系列中深入剖析Socket的几篇文章。

    3、JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中。

    4、虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。

【深入Java虚拟机】之八:Java垃圾收集机制(转)相关推荐

  1. java垃圾收集的目的_()、()和()使Java的设计目的得以实现?A、Java虚拟机B、垃圾收集机制C、三级代码安全检查机制D、Serv...

    提出德育的认知模式的学者是____. 与引起心肌收缩性减弱的基本机制无关的是()A.心肌收缩的蛋白(收缩蛋白.调节蛋白)被破坏B.心肌 企业应当在利润表中分别列示()和终止经营损益.A.持续经营收入B ...

  2. 0x00000000指令引用的内存不能为written_「深入Java虚拟机」Java内存区域与内存溢出...

    内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域.Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器.Java虚拟机栈.本地方法栈 ...

  3. Java虚拟机 和 java虚拟机下的进程

    一.什么是Java虚拟机      当你谈到Java虚拟机时,你可能是指:      1.抽象的Java虚拟机规范      2.一个具体的Java虚拟机实现      3.一个运行的Java虚拟机实 ...

  4. java虚拟机原理(java虚拟机的基本结构)

    虚拟机的分类 虚拟机大体上分为系统虚拟机和程序虚拟机: 如:大名鼎鼎的VMware就属于系统虚拟机,VMware是完全对物理计算机的仿真,提供一个可以运行完整操作系统的软件平台: 程序虚拟机的代表就是 ...

  5. 深入理解 Java 虚拟机——走近 Java

    1.1 - 概述 Java 总述:Java 不仅是一门编程语言,还是一个由一系列 计算机软件 和 规范 形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于 嵌入式 ...

  6. java虚拟机系列:java虚拟机内存模型

    java内存模型,分为程序计数器,虚拟机栈,本地方法栈,java堆,java栈.根据受访的权限不同设置,可以分为线程共享和线程私有.线程共享指可以允许所有的线程共享访问的一类内存区域,包括堆内存区,方 ...

  7. 深入理解Java虚拟机-走近Java

    本博客主要参考周志明老师的<深入理解Java虚拟机>第二版 读书是一种跟大神的交流.阅读<深入理解Java虚拟机>受益匪浅,对Java虚拟机有初步的认识.这里写博客主要出于以下 ...

  8. [Java学习探讨]为什么学Java虚拟机的Java程序员更有价值?

    个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈 [Java学习探讨]为什么学Java虚拟机的Java程序员更值钱? 曾经的我经常害怕处理与JVM相关的异常,对JVM的配置参数也一无 ...

  9. 《深入理解Java虚拟机》Java内存区域与内存溢出异常

    <深入理解Java虚拟机>Java内存区域与内存溢出异常 参考文章: (1)<深入理解Java虚拟机>Java内存区域与内存溢出异常 (2)https://www.cnblog ...

  10. Java虚拟机规范 Java SE 8版 - class文件格式(二)

    Java虚拟机规范 Java SE 8版 - class文件格式(二) 4.5 字段 4.6 方法 4.7 属性 4.7.1 自定义和命名新的属性 4.7.2 ConstantValue 属性 4.7 ...

最新文章

  1. C#中使用DateTimePicker控件显示修改日期时间
  2. nginx启动初始化过程(二)
  3. LeetCode 1274. 矩形内船只的数目(分治)
  4. 七年级上册计算机重点知识点,初一上册数学重点知识点
  5. 我的网络安全第三次实验汇报
  6. 10个线程同时执行i++操作1000次,如何保证结果是1w
  7. esp连接服务器的协议,【零知ESP8266教程】WIFI TCP协议通信 TCP服务器示例
  8. 如何安装红旗linux6.0声卡驱动
  9. KEIL5下载时提示“keil5 notarget connected”
  10. 在线qq客服的html代码生成器,js生成qq客服在线代码
  11. 购物全返模式是什么?解析购物的盈利模式
  12. 物联网应用系统三层结构的设计_基于物联网技术的智能油烟在线监测系统的设计与应用...
  13. [日记] 招行的服务就是不错。。。。
  14. Mac 安装7z解压工具
  15. 初识SecureCRT工具
  16. SII9136 调试出来, 欣喜若狂!(需要 SII9136 资料的请联系我!)
  17. java消息总线ibus_IBUS智能照明总线系统的应用
  18. NeuralProphet之二:季节性(Seasonality)
  19. 企业管理者的基本操作
  20. 计算机应用基础网络核心课程,[高职院校《计算机应用基础》“核心+拓展”课程建设初探] 计算机应用基础 2018...

热门文章

  1. 华为天才少年自制机械臂!能给葡萄缝针的那种,成本1万块,网友:能把脑子开源一下?...
  2. 小米手机相册选择并裁剪图片
  3. excel2013使用分列功能拆分数据
  4. Python+wxWidgets快速开发桌面小程序
  5. vat可以退税吗_英国VAT退税流程以及VAT退税政策
  6. 6-18 提桶寻宝 (10 分)
  7. 发布 'xx' 的并发快照不可用,因为该快照尚未完全生成,或者日志读取器代理未运行,无法激活它。如果并发快照的生成过程中断,则必须重新启动用于该发布的快照代理,直到生成完整的快照。
  8. 网络工程师的昨天今天明天
  9. 【婚礼司仪】婚礼主持稿
  10. NOAA官网下载数据