前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为什么HotSpot有‘对象’,因为他会垃圾分类和回收

为什么HotSpot有‘对象’,因为他会垃圾分类和回收

作者头像
胖虎
发布2019-07-10 15:33:49
3790
发布2019-07-10 15:33:49
举报
文章被收录于专栏:晏霖晏霖

曾经有人关注了我

后来他有了女朋友

前言

想和HotSpot一样吗?安全的管理众多的对象,掌握着对象的生与死。前文我们了解了对象在内存中的整个生命周期的是什么样的,程序运行就要无限的创建对象,我们Java 虚拟机一定要对对象进行控制,不能让其泛滥,所以Java 虚拟机有一个特别重要的功能就是垃圾回收,把没用的对象进行回收,释放内存,这样程序才能稳定的在虚拟机上运行。

正文

人有英年早逝、长命百岁,对象也有存活时间长短和年龄之分。

Java 虚拟机是按照分代的方式进行垃圾收集,每一个年代上分别对应着多种多样的垃圾收集器,每一个垃圾收集器都会按照特定的算法进行工作。

Java 虚拟机主要是对堆中的对象进行垃圾回收的,一般的我们把堆分为新生代和老年代,由于新生代都是一些不成熟的对象,所以这里98%的对象都是朝生夕死,故将新生代又细划分为一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存)区,两块空间的比例默认是8:1。

每次使用Eden区和一块Survivor,将Eden和Survivor中存活的对象一次性复制到另一个Survivor区,然后清理掉刚用过的Eden和Survivor空间。如果按照这样的分法

新生代其实是这样的结构:Eden:Survivor1:Survivor2=8:1:1

我们刚刚清理了Eden+Survivor1(80%+10%)的空间,把存活的复制到Survivor2空间。我们下次继续清理就要对Eden+Survivor2,加上Survivor2原有存活的对象。我们没有办法保证每次幸存下来的对象不多于10%,新生代反复复制多次,如果其中一个Survivor空间不足,就需要老年代进行分配担保。

分配担保就是类似于银行贷款的担保人,借款人还不上担保人要偿还。原本新生代生成的对象自己完全可以收回,如果哪一次自己吃不下自己生产的对象,就要把这些对象全权托付给老年代进行管理。老年代其实就是一个巨坑,所有能在老年代的对象都是不好对付的,这里的垃圾回收频率要低于新生代十多倍左右,新生代频繁复制十多次,老年代才会回收一次。

故,目前有三种情况对象可以进入老年代

  • 第一种通过担保方式,上面刚提到
  • 第二种就是大对象,jvm可以设定值,如果对象过大,或者数组啊,会直接放入老年代。
  • 第三种就是按照年龄算,新生代内每次gc的时候,如果对象还存活,就会给年龄加1,如果大于默认的15或者相同的年龄大于内存的一半的时候可以不达到设定年龄,就会转移到老年代。

以上内容是否让大家明白了我们垃圾回收主要区域 ->堆 是什么样子了吧.

接下来就要说一下在这个堆里面到底使用了哪些算法。

标记-清除

这是所有垃圾收集算法中最基础的,分为“标记”和“清除”两个阶段。首先标记出需要回收的对象,然后统一回收所有被标记的对象。他是最基础的原因是因为后续的算法都是基于他改进的,弥补了他的不足,他的不足有两点: 第一是效率问题,标记和清除的效率都不高。其实最主要的原因是因为,标记清除后造成了不连续的内存碎片,导致大对象不能存储。我们用图例可以清楚的看出来。

复制算法

将内存按容量平均分为两半,保证一半是空的,一半是正在使用的。gc的时候把存活的对象复制到空的那一半,然后清空这一半。

这样做优点在于每次清除最多一半的内存,效率大大提升,第二就是解决了内存碎片化问题。

缺点就是空间利用率不高,所以我在开篇前给大家提前科普的,新生代分为三块区域来回复制的原因。聪明的小朋友读到这已经知道了吧,新生代用的就是复制算法

就是因为新生代都是朝生夕死,收集频繁,满足复制算法的特性。

标记-整理

标记-整理和标记-清除中的标记是不是一样啊,答案是肯定的,标记-整理相对于标记-清除一个很明显的区别在于“整理” ,因为有了整理的过程,该算法解决了内存碎片化的问题。

该算法工作原理是:在标记了需要清除对象后,并不是直接进行清除,而是让所有的存活对象向前移动,然后清除掉其余的内存。

到目前位置我们知道了

堆中年代堆分布结构

学习了垃圾收集的算法

接下来就来学习一下在真正的HotSpot中是如何将这些算法实现的。

其实HotSpot也是利用了分代的特性和上述基础算法结合实现的,只不过在实现上对算法执行效率有严格要求。

我们前文提到过,在HotSpot中判断对象的是否可被回收的算法是可达性算法,可达性算法的核心在于对象有没有存在GC Root的引用链上,然而GC Roots的节点主要存在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的局部变量表)中,虚拟机需要在他全部内存区去逐个检查这里面的引用,哪些是GC Root,然后才能判断出对象是否在引用链上。

HotSpot会这么傻?真的是逐个检查吗?当然不会。

安全点

在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

简单说OopMap数据结构式存储对象在栈中的引用地址。

OopMap的帮助下我们可以快速的完成GC Roots的枚举。

虚拟机不可能在任何有对象的地方都会生成OopMap,这样的话GC成本会很高,所以只是在特定的位置记录了这些信息,例如:方法调用、循环跳转、异常跳转等,这些位置被称为安全点(SafePoint)

安全点太少GC停顿时间会增加,安全点过多GC会很频繁,所以安全点的选定基本上是以程序是否具有让程序长时间执行的特征为标准进行选定的。

我们知道GC发生时所有工作的线程必须挂起,全部要暂停下来,那么怎么让GC发生时所有线程都跑到安全点进行GC呢?

有两种方式:

抢先式中断(Preemptive Suspension):抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

主动式中断(Voluntary Suspension):主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区

使用安全点似乎已经完美解决了如何进入GC的问题?

但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。

那么HotSpot就要在线程sleep和blocked前先走到安全区域,然后先标识自己已经进入了安全区,接下来你该阻塞就阻塞,该睡觉就睡觉,这个区域就是安全区域,只要在安全区域都是安全点。

下面是安全区域的概念,还是比较难理解,不如就按照我的白话文理解容易些。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。

你线程这么厉害?我HotSpot的安全区你想来就来想走就走?有些线程还留宿一夜?

走也是有条件的

当线程要离开安全区域时,他要检查系统是否完成了根节点枚举或者当前这个GC结束,如果都ok,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。

到目前为止,一个对象是否需要被回收,以及已经整个GC全过程就结束了。这部分内容也是HotSpot比较重要的内容之一,因为文章篇幅和笔者能力有限,很多细节没有说透,不过读者可以加微信私下探讨。

谢谢支持,原创不易。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 晏霖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档