深入理解JVM之二:内存的垃圾回收算法

在接收垃圾回收算法是有必要对以下知识点了解熟知:

哪些内存需要回收

jvm虚拟机内存结构中程序计数器、本地方法栈、虚拟机方法栈都是线程私有的,随着线程的创建被创建,随着线程的销毁而推出,栈帧随着方法的创建以及退出有条不紊的实现入栈与出栈。而在程序运行期间才能知道要创建多少对象,这部分内存是不确定的动态的,因此java虚拟机的垃圾回收机制主要是java堆中和方法区中内存回收。

如何确定一个对象可以被回收(可达性分析)

可达性分析算法是通过一系列成为“GC-Root”的对象作为起始点,从上向下索引不在引用链上的对象为不可用对象(可回收对象),“GC-Root”对象包括以下几种(虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象)。当然不Reference引用链上的方法也不一定马上回收需要经过两次标记(可达性分析没有在引用链上的对象会被第一次标记并进行筛选,筛选的条件是:这些对象是否有必要执行finalize()方法,如果有必要,这些对象会被加入到一个队列中,稍后GC将会对队列中的对象进行第二次小范围的标记,对象在执行finalize()方法时有最后一次机会-和引用链上的对象建立关联,不过finalize()方法只会执行一次);

方法区(永久代)对象回收:

主要分为废弃常量无用的类;废弃的常量的回收过程和java对中回收的果茶差不多;但要判定一个类为无用的类条件比较苛刻(该类的所有实例已经被回收、加载该类的ClassLoader已经被回收、该类对应的java.lang.class对象没有在任何地方被引用

目前主要存在三种垃圾清理算法:标记-清理(Mark-Sweep)、复制(Copying)、标记-整理(Mark-Compact)了解了以上知识点马上来看看垃圾回收算法:

标记-清理

标记-清理算法(mark-Sweep):分为两个阶段:标记和清理。标记采用可达性分析,然后再统一的清理;这种算法存在两方面的问题:(1)效率问题,标记和清理的效率都不高;(2)空间问题:这种算法清理完内存碎片太多不连续空间较多没在程序运行中需要分配较大的内存是存在无法满足条件的情况。

复制算法:

赋值算法将内存分为两块,每次只使用其中的一块,当一块内存使用完时候就将还存活的对象复制到另外一块。极大的提高了效率问题,但对于将内存分为相等的两块代价比较大,因此商用虚拟机采用这种算法回收新生代,将内存划分为Eden空间、From Survivor空间、To Survivor空间,每次只使用Eden空间和其中的一块Survivor空间,复制算法在对象存活率比较低的内存具有较高的回收效率。

标记-整理算法

先使用可达性分析算法对内存对象进行标记,在标记后将存活的对象移动到内存一段,然后将边界以外的内存进行清理。

分代收集算法

了解完了目目前商用虚拟机都使用“分代收集算法”,所谓分代就就是根据对象的生命周期把内存分为几块,一般把Java堆中分为新生代和老年代,这样就可以根据对象的“年龄”选择合适的垃圾回收算法。垃圾回收时,新生代对象中都会有大批量的对象死亡,就选择复制算法(因为存活的对象较少,而死亡的对象过多,如果使用标记-清除算法的话,需要遍历标记,显然效率较低,而使用复制算法就可以把存活的较少的对象复制到可用内存区域中,这样效率就较高);对于老年代对象,其存活率较高,所以就可以使用“标记-清除”算法或者“标记-整理”算法。

HotSpot垃圾回收算法的实现

我们回到标记-清除算法,在清除阶段,为了枚举未被标记的对象,所以需要从根节点(GC Roots)开始查找引用链,这个过程会导致GC停顿,意思就是在GC的时候Java的执行线程都被停顿,好像被冻结在某一个时间点,也叫“Stop the world”。然而目前主流的Java虚拟机都是用准确式GC(所谓准确式GC,即使虚拟机知道内存中的某个位置的数据是什么类型),当“Stop the world”的时候并不需要检查所有的引用位置,虚拟机通过使用OopMap这个数据结构知道哪些地方存放着对象的引用。


现在我们使用OopMap,虚拟机已经知道哪些位置存放着对象,从而GC Roots可以迅速的枚举可达对象的引用链。但是问题来了:是不是需要对所有的指令都使用OopMap呢?答案是否定的。实际上,虚拟机只在“特定的位置”记录了对象的引用信息,比如我们使用方法调用或者循环的时候,就会设定这样的位置,如果越过这个位置的继续执行指令,然而程序是不允许因为指令流长度太长而执行过长时间,所以这个“特定位置“就成为了程序是否具有长时间运行的分界点。这个”特定的位置“也称为安全点。


现在虚拟机有了安全点,于是只会到安全点寻找对象的引用信息,并且在安全点暂停Java执行线程,然而还有一个问题:如果一个线程的执行位置距离安全点比较远怎么办呢?在Hotspot使用主动式中断中断执行线程,其思想如下:当GC的时候不需要直接对线程操作以中断线程,仅仅是设置一个标志,然后让执行线程去轮询这个标志,发现中断标志为真的时候就自己中断线程。需要注意的是,轮询标志的地方与安全点的位置是重合的,另外再加上创建对象需要分配的地方。现在有一个问题:什么情况下,轮询标志才会为真呢?(这个需要根据执行的时候指令来确定,属于机器级别的指令)
OK,现在通过GC Roots和安全点,程序能够在不太长的时间就可以到达安全点,并暂停执行线程。那么如果程序在阻塞或者睡眠的状态的时候,执行线程如何中断呢?想象这么一个场景,去电影院买票,你在排队,但是由于人太多你一直在等待,所以一直没买着票这时即使售票员说了票卖完了,但是人太多太吵很多人都没听到,所以你仍然在排队。对于这种情况,JVM设置了安全区域。安全区域就是指在这个区域内,对象的引用关系不会发生改变,在这个范围暂停线程都是可以的,枚举根节点的时候,得到的引用信息还是完整的。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HotSpot的垃圾回收机制采用分代回收,堆分为年轻代和老年代,非堆就是持久带。面对不同的代,采用不同的回收策略。 而年轻代又可以分为1个Eden和2个Survivor。对于Eden,大多数对象都是先在此区域开辟空间,存储年轻对象的实例数据。Eden相对于Old带,空间是比较小的。所以对象数据不断地在此开辟空间,Eden不够了,年轻代发生了回收。放置(稍后会说怎么放置)到S1或者S2。S1或者S2不够放了,直接放到Old带。根据年轻代的特点,空间小,发生回收事件频率较高,那么就采用标记-复制的算法将Eden中的对象实例数据克隆到S1或者S2或者直接克隆到Old带。标记-复制算法优点是:快速、节省内存碎片。缺点是:内存消耗的空间需要变为原来的50%,另一块空间作为复制的目标。 老年代因为空间比较大,存储的对象又是比较长寿的对象,所以采用标记-整理或者也称作标记-压缩算法。这样不必开辟另外50%的空间用于复制目标,也不用担心这个较为辽阔的内存空间产生占用碎片的问题。缺点就是又标记、又压缩的。对于Old带比较费时间。 持久带虽然资料上没有明说,但是根据持久带的作用和特点以及触发该区回收的情景可以推算,持久带采用的回收算法应该是标记-整理/压缩算法JVM内存回收只要是针对这3个区域来说的。像NIO的直接文件内存读取,使用的是直接内存,只有出发了FullGC方能回收该区域!JNI调用本地库,本地代码所消耗的内存需要操作系统额外开辟内存

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值