文档章节

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

htq
 htq
发布于 2016/07/26 09:38
字数 4013
阅读 3
收藏 0
点赞 0
评论 0

【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岁),就将会被移动到老年代中。


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


© 著作权归作者所有

共有 人打赏支持
htq

htq

粉丝 19
博文 67
码字总数 1007
作品 3
武汉
聊聊JAVA虚拟机中的垃圾收集器

前言 JAVA虚拟机的垃圾收集器是虚拟机内存的清道夫,它的存在让JAVA开发人员能将更多精力投入到业务研发上。了解垃圾收集器,并利用好这个工具,能更好的保障服务稳定性。这篇文章通过分析J...

lilugoodjob ⋅ 05/13 ⋅ 0

热修复与插件化基础——Java与Android虚拟机

一、Java虚拟机(JVM) 1、JVM整体结构 使用javac将java文件编译成class文件。 类加载器(ClassLoader)将class字节码加载进JVM对应的内存中。 JVM将内存分配给方法区、堆区、栈区、本地方式...

CSDN_LQR ⋅ 05/13 ⋅ 0

JAVA虚拟机 JVM 详细分析 原理和优化(个人经验+网络搜集整理学习)

JVM是java实现跨平台的主要依赖就不具体解释它是什么了 ,简单说就是把java的代码转化为操作系统能识别的命令去执行,下面直接讲一下它的组成 1.ClassLoader(类加载器) 加载Class 文件到内...

小海bug ⋅ 06/14 ⋅ 0

Java 面试知识点解析(三)——JVM篇

前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大部...

我没有三颗心脏 ⋅ 05/16 ⋅ 0

培训云计算学校,虚拟机基本结构讲解

我们要对JVM虚拟机的结构有一个感性的认知。毕竟我们不是编程人员,认知程度达不到那么深入。一个运行时的Java虚拟机实例的天职是:负责运行一个java程序。当启动一个Java程序时,一个虚拟机...

长沙千锋 ⋅ 05/17 ⋅ 0

《成神之路-基础篇》JVM——垃圾回收(已完结)

Java内存模型,Java内存管理,Java堆和栈,垃圾回收 本文是[《成神之路系列文章》][1]的第一篇,主要是关于JVM的一些介绍。 持续更新中 Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收 ...

⋅ 05/05 ⋅ 0

JVM自动内存管理机制—读这篇就够了

之前看过JVM的相关知识,当时没有留下任何学习成果物,有些遗憾。这次重新复习了下,并通过博客来做下笔记(只能记录一部分,因为写博客真的很花时间),也给其他同行一些知识分享。 Java自动内...

java高级架构牛人 ⋅ 06/13 ⋅ 0

面试中关于Java虚拟机(jvm)的问题看这篇就够了

最近看书的过程中整理了一些面试题,面试题以及答案都在我的文章中有所提到,希望你能在以问题为导向的过程中掌握虚拟机的核心知识。面试毕竟是面试,核心知识我们还是要掌握的,加油~~~ 下面...

snailclimb ⋅ 05/12 ⋅ 0

00.java虚拟机的基本结构概念

java虚拟机的基本结构 1、类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。 2、方法区:就是存放类信息、常量信息、常量池信息、包括字...

挚爱冷如烟° ⋅ 05/08 ⋅ 0

一篇简单易懂的原理文章,让你把JVM玩弄与手掌之中

jvm原理 Java虚拟机是整个java平台的基石,是java技术实现硬件无关和操作系统无关的关键环节,是java语言生成极小体积的编译代码的运行平台,是保护用户机器免受恶意代码侵袭的保护屏障。JVM...

烂猪皮 ⋅ 05/08 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

磁盘管理—逻辑卷lvm

4.10-4.12 lvm 操作流程: 磁盘分区-->创建物理卷-->划分为卷组-->划分成逻辑卷-->格式化、挂载-->扩容。 磁盘分区 注: 创建分区时需要更改其文件类型为lvm(代码8e) 分区 3 已设置为 Linu...

弓正 ⋅ 5分钟前 ⋅ 0

Spring源码解析(六)——实例创建(上)

前言 经过前期所有的准备工作,Spring已经获取到需要创建实例的 beanName 和对应创建所需要信息 BeanDefinition,接下来就是实例创建的过程,由于该过程涉及到大量源码,所以将分为多个章节进...

MarvelCode ⋅ 25分钟前 ⋅ 0

a href="#"

<a href="#">是链接到本页,因为你有的时候需要有个链接的样式,但是又不希望他跳转,这样写,你可以把这个页面去试试

颖伙虫 ⋅ 32分钟前 ⋅ 0

js模拟栈和队列

栈和队列 栈:LIFO(先进后出)一种数据结构 队列:LILO(先进先出)一种数据结构 使用的js方法 1.push();可以接收任意数量的参数,把它们逐个推进队尾(数组末尾),并返回修改后的数组长度。 2....

LIAOJIN1 ⋅ 32分钟前 ⋅ 0

180619-Yaml文件语法及读写小结

Yaml文件小结 Yaml文件有自己独立的语法,常用作配置文件使用,相比较于xml和json而言,减少很多不必要的标签或者括号,阅读也更加清晰简单;本篇主要介绍下YAML文件的基本语法,以及如何在J...

小灰灰Blog ⋅ 40分钟前 ⋅ 0

IEC60870-5-104规约传送原因

1:周期循环2:背景扫描3:自发4:初始化5:请求6:激活7:激活确认8:停止激活9:停止激活确认10:激活结束11:远程命令引起的返送信息12:当地命令引起的返送信息13:文件传送20:响应总召...

始终初心 ⋅ 53分钟前 ⋅ 0

【图文经典版】冒泡排序

1、可视化排序过程 对{ 6, 5, 3, 1, 8, 7, 2, 4 }进行冒泡排序的可视化动态过程如下 2、代码实现    public void contextLoads() {// 冒泡排序int[] a = { 6, 5, 3, 1, 8, 7, 2, ...

pocher ⋅ 今天 ⋅ 0

ORA-12537 TNS-12560 TNS-00530 ora-609解决

oracle 11g不能连接,卡住,ORA-12537 TNS-12560 TNS-00530 TNS-12502 tns-12505 ora-609 Windows Error: 54: Unknown error 解决方案。 今天折腾了一下午,为了查这个问题。。找了N多方案,...

lanybass ⋅ 今天 ⋅ 0

IDEA反向映射Mybatis

1.首先在pom文件的plugins中添加maven对mybatis-generator插件的支持 ` <!-- mybatis逆向工程 --><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-ma......

lichengyou20 ⋅ 今天 ⋅ 0

4.10/4.11/4.12 lvm讲解 4.13 磁盘故障小案例

准备磁盘分区 fdisk /dev/sdb n 创建三个新分区,分别1G t 改变分区类型为8e 准备物理卷 pvcreate /dev/sdb1 pvcreate /dev/sdb2 pvcreate /dev/sdb3 pvdisplay/pvs 列出当前的物理卷 pvremo...

Linux_老吴 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部