在【java虚拟机系列】java虚拟机系列之JVM总述中我们已经详细讲解过java中的内存模型,了解了关于JVM中内存管理的基本知识,接下来本博客将带领大家了解java中的垃圾回收与内存分配策略。

垃圾回收(Garbage Collection,GC)是java语言的一大特色,在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。而在C/C++中是需要程序员主动释放的,而在java中则交给JVM自动完成,既然是交给程序自动执行,那么这里就必须完成以下几件事:

1哪些内存需要回收?(即哪些对象可以被看做是”垃圾“)

2如何回收?(即常用的垃圾回收算法)

3内存分配策略

接下来就按照上述提出的三个疑问一一进行详细讲解。

一哪些内存需要回收?

通过前面的【java虚拟机系列】java虚拟机系列之JVM总述我们知道,java内存区域主要指的是java运行时数据区,在这个内存区域中,程序计数器,java栈,本地方法栈3个区域随着线程的产生而产生,随着线程的消亡而消亡。因此这些地方不需要过多的考虑内存回收,因为线程结束后内存自然也就跟着回收了,而java堆区与方法区则不然,一个接口中的多个实现类需要的内存可能不一样,因此当程序在运行时才能够知道会创建哪些对象,因此这部分内存的回收与分配是动态的,这也是垃圾回收关注的内存区域。

那么堆或方法区中的哪些对象可以被看做是“垃圾”呢?即哪些对象应该被回收呢?

这就涉及到jvm的垃圾判定算法,常用的垃圾判定算法包括:引用计数算法,可达性分析算法。下面一一介绍

引用计数算法:

我们知道java堆中的内存是通过引用来访问的,即每一个堆内存都对应着一个可以访问该内存地址的引用,那么当某个引用指向这块堆内存时,让计数器加1,当指向该堆内存的引用无效时计数器减1,那么很清楚的知道当该对象的计数器为0时,即表示该对象可视为”垃圾“被回收。

通过其原理可以知道,该算法实现简单,判定效率很高,但是目前主流的JVM都没采用该算法来管理内存,最主要的原因是该算法很难解决对象之间的循环引用的情况。举个例子如下:

class TestX{public TestY y;}
class TestY{public TestX x;
}
public class Main{public static void main(String[] args){X x = new X();Y y = new Y();x.y=y;y.x=x;//这两行赋值完成后x与y存在相互引用x = null;y = null;
<span style="white-space:pre"> </span>    System.gc();//通知虚拟机回收}
}

虽然通过x = null;  y = null;两行语句将X与Y的引用置空,表示当前堆中的X与Y无引用指向它们,因此它们已经不能被访问到,按道理应该被垃圾回收器回收,但是因为x与y互相引用,导致x与y的引用计数器都不为0,因此如果采用引用计数器算法的话,那么这两个对象的内存都不能被回收。运行程序,查看运行结果,可以从内存分析看到,事实上这两个对象的内存被回收,这也从侧面说明了当前主流的JVM都不是采用的程序计数器算法作为垃圾判定算法的。

可达性分析算法:

可达性分析算法是java语言所采用判定对象是否存活的算法,该算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

因此该算法的关键是”GC Roots”的对象的选取,在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区中类静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI(即一般说的Native方法)引用的对象。

注意:在可达性分析算法中,不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否需要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“不需要要执行”。注意任何对象的finalize()方法只会被系统自动执行1次。

如果这个对象被判定为需要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。因此调用finalize()方法不代表该方法中代码能够完全被执行。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。从如下代码中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。

/**  * 此代码演示了两点:  * 1.对象可以在被GC时自我拯救。  * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次  */
public class FinalizeEscapeGC {  public static FinalizeEscapeGC SAVE_HOOK = null;  public void isAlive() {  System.out.println("yes, i am still alive :)");  }  @Override  protected void finalize() throws Throwable {  super.finalize();  System.out.println("finalize mehtod executed!");  FinalizeEscapeGC.SAVE_HOOK = this;  }  public static void main(String[] args) throws Throwable {  SAVE_HOOK = new FinalizeEscapeGC();  //对象第一次成功拯救自己  SAVE_HOOK = null;  System.gc();  //因为finalize方法优先级很低,所以暂停0.5秒以等待它  Thread.sleep(500);  if (SAVE_HOOK != null) {  SAVE_HOOK.isAlive();  } else {  System.out.println("no, i am dead :(");  }  //下面这段代码与上面的完全相同,但是这次自救却失败了  SAVE_HOOK = null;  System.gc();  //因为finalize方法优先级很低,所以暂停0.5秒以等待它  Thread.sleep(500);  if (SAVE_HOOK != null) {  SAVE_HOOK.isAlive();  } else {  System.out.println("no, i am dead :(");  }  }
} 

运行结果如下:

finalize mehtod executed!
yes, i am still alive :)
no, i am dead :( 

从运行结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器调用过,且在被收集前成功逃脱了。

另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

二如何回收?(常用的垃圾回收算法)

常用的垃圾回收算法包括:标记-清除算法,复制算法,标记-整理算法,分代收集算法,下面一一介绍其实现原理。

标记-清除算法(Mark-Sweep):最基础的垃圾回收算法,顾名思义,包括标记与清除两个过程。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实就是前面介绍过的可达性分析算法的过程。

不足点:

1效率不高,标记和清除两个过程的效率都不高。

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

复制算法:赋值算法是为了解决标记-清除算法的空间利用率不高而改进的,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。算法执行过程如下图所示:

很显然该算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

标记-整理算法(Mark-Compact):复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。而是采用标记-整理算法(Mark-Compact)。

该算法的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,示意图如下:

分代收集算法(Generational Collection):该算法是当前绝大多数虚拟机采用的垃圾收集算法,该算法是综合考虑上述几种算法的最佳情况,根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。


注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。


三java中的内存分配与回收策略

java的自动内存管理事实上自动的解决了两个问题:给对象分配内存以及回收分配给对象的内存。关于内存回收,前面已经详细介绍过,因此接下来重点讲解java中的内存分配技术。

对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的Eden ,少数情况下会直接分配在老年代,分配的规则虽不是百分之百固定的,但也遵循以下几个规则:

1对象优先在Eden分配:

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。下面是新生代内存区域图:

在GC的过程中,会将Eden Space和From  Space中的存活对象移动到To Space,然后将Eden Space和From Survivor进行清理。如果在清理的过程中,To Survivor无法足够来存储某个对象,就会将该对象移动到老年代中。

如果在GC过程中,To Space无法存储某个对象,就会将该对象移动到老年代中。

2大对象直接进入老年代:

所谓的大对象是指需要大量连续存储空间的对象,最常见的大对象如很长的字符串与很大的数组,之所以将大对象直接在老年代分配是为了避免在Eden区与两个Survivor区进行大量的内存复制,注意在新生代采用的是复制算法收集垃圾对象。

3长期存活的对象进入老年代:

前面说过虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生且经过第一次Minor GC后仍然存活,且能被Survivor容纳的话将被移动到Survivor空间中,然后将该对象年龄设为1。对象在Survivor区中每“躲过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被移动到老年代中。


以上就是本博客的主要内容,如果读者觉得不错,记得小手一抖,点个赞哦!另外欢迎大家关注我的博客账号哦,将会不定期的为大家分享技术干货,福利多多哦!

转载于:https://www.cnblogs.com/hainange/p/6334013.html

【java虚拟机序列】java中的垃圾回收与内存分配策略相关推荐

  1. 《深入理解Java虚拟机》-----第3章 垃圾收集器与内存分配策略

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来. 3.1 概述 说起垃圾收集(Garbage Collection,G ...

  2. 深入理解java虚拟机-读书笔记2-垃圾收集器和内存分配策略

    垃圾回收重点区域:堆和方法区部分区域. 引用计数算法: 1,引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器都为0的对象 ...

  3. 《深入理解Java虚拟机第3版》垃圾收集器与内存分配策略、虚拟机性能监控故障处理工具

    目录 往期博客:Java课堂篇3_初识JMM.常量池简单理解(字符串常量池.静态常量池.大整型常量池) 为什么要了解垃圾收集和内存分配? 如何判断对象已死? 引用计数算法 可达性分析算法 JDK1.2 ...

  4. Java虚拟机(二)——垃圾回收与内存分配

    文章目录 垃圾回收与内存分配 1 引用 2 如何判断对象需要回收 2.1 引用计数算法 2.2 可达性分析算法 2.3 废弃常量的回收 2.4 无用的类的回收 3 内存分配 4 垃圾收集算法 4.1 ...

  5. Java虚拟机的垃圾回收器以及内存分配策略详解

    概述 垃圾回收器(GC)是什么以及为什么我们需要垃圾回收器?? 垃圾回收是Java语言区别于其他语言的一种最为重要的特性之一, 通过垃圾回收器(Garbage Collection)来实现对我们Jav ...

  6. 深入理解Java虚拟机-垃圾回收器与内存分配策略

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

  7. 垃圾回收器和内存分配策略

    本文作者:李敏,叩丁狼高级讲师.原创文章,转载请注明出处. 4. 垃圾回收器和内存分配策略 **GC(Garbage Collection)**的历史比java久远.1960年诞生于MIT的Lisp是 ...

  8. 垃圾回收器与内存分配策略

    垃圾回收器与内存分配策略 1. 前言 计数器:计数器难以解决循环引用,需要大量的额外处理才能正确工作. 可达性分析算法 在java技术栈里GC Roots包括以下几种 虚拟机栈中引用的对象 方法区中类 ...

  9. Java 垃圾回收器与内存分配策略 JVM

    文章目录 什么是垃圾回收? 哪些位置内存需要回收? 四种引用关系 如何判断一个对象是不是需要回收? GC Roots 对象有哪些? 如何回收? 垃圾回收算法的指导思想 常用的垃圾回收算法 看图即可明白 ...

最新文章

  1. 014——数组(十四)array_reduce array_slice array_splice array_sum
  2. netapp学习(十三)---Snapshot基础知识(上)
  3. 使用淘宝镜像cnpm安装Vue.js
  4. Android UI 常用图标尺寸规范
  5. ViewData 和 ViewBag 到底有什么区别?
  6. .NET Core实战项目之CMS 第十一章 开发篇-数据库生成及实体代码生成器开发
  7. 论文浅尝 | Distant Supervision for Relation Extraction
  8. linux 进程退出原因,linux – 为什么waitpid不等待进程退出?
  9. 【算法笔记】:区间覆盖问题:贪心算法
  10. 收集:Hibernate中常见问题 No row with the given identifier exists问题的原因及解决
  11. pyautogui的两天坑moveto图像识别
  12. H桥驱动芯片IR2110功能简介
  13. 58节沈大海H5edu.cn2016javaScript视频教程打包下载
  14. postgresql客户端远程连接
  15. 从Unity商店下载的插件存放的位置
  16. 基于Harry Potter的数据可视化
  17. DaVinci:RGB 混合器
  18. Open Explorer Plugin for Eclipse (eclipse 插件 在ecli
  19. 软件导出excel时提示没有注册类
  20. 微服务下权限校验方案

热门文章

  1. 如何修改WSS站点的主菜单
  2. 总结之:CentOS 6.5 LAMP分主机平台的搭建及测试
  3. 中澳科学家在量子安全通信领域合作研究取得突破性进展
  4. 读zepto源码之工具函数
  5. Intellij IDEA 14.x 菜单项中Compile、Make和Build的区别
  6. CentOS 6.7 x64 使用pptpd搭建***服务器
  7. IEC61850报告服务(报文解析)
  8. 【POJ】1276 Cash Machine 【背包问题】
  9. 给各位读者朋友们、热心访问本博客的朋友们的一份新年贺卡
  10. Flink时间属性和窗口