前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM学习笔记——垃圾收集器与内存分配策略(1)

JVM学习笔记——垃圾收集器与内存分配策略(1)

作者头像
用户1665735
发布2019-02-19 18:20:37
3680
发布2019-02-19 18:20:37
举报
文章被收录于专栏:kevindroidkevindroid

概述

上一篇文章介绍了java运行时内存的各个区域,其中虚拟机栈,程序计数器,本地方法栈三个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈与入栈的操作,每一个栈帧分配多少内存基本是类结构确定下来就已知的。因此,这几个区域的内存回收都具有确定性,在这几个区域不必过多的考虑回收的问题,因为方法结束或者线程结束时,内存自然也跟着回收了。 而java堆与方法区不一样,一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能不一样,只有在程序处于运行期间才能知道会创建哪些对象。这部分的内存分配与回收都是动态的,垃圾收集器所关心的也是这部分内存。

对象“已死”吗?

很明显,垃圾回收器在回收对象前需要确定,哪些对象仍然存活,哪些对象已经死去。

引用计数分析算法

一种简单的判断对象是否存活的方法:给对象添加一个引用计数器,当有一个地方引用它时,计数器加一;引用失效时,计数器减一。任何时刻计数器为0的对象就是不可能再被引用的,这种方法实现简单,判定效率高,部分情况下也很不错,但是java并没有采取这种算法,主要的问题时没有解决对象间循环引用的问题。

public class testGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    //下面这个属性的作用就是占点内存用来判断是否被回收过
    private byte[] bigSize = new byte[2 * _1MB];
    public static  void testGC(){
        testGC objA = new testGC();
        testGC objB = new testGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;
        System.gc();
    }
}

上面代码中的objA与objB互相持有对方的引用,虽然计数器都不为0,实际上这两个对象已经不可能再被访问。

可达性分析算法

这个算法的基本思路就是通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径被称为应用链(Reference Chain),当一个对象到GC Roots没有任何引用链时(用图论的说法就是这两个对象不可达),则证明此对象时不可达的。

这里写图片描述
这里写图片描述

在上图中,明显对象5,6,7是不可达的,会被判定为可回收的对象。 在java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

再谈引用

jdk1.2之前,对应用的定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表引用。这种定义无法描述一些“食之无味,弃之可惜”的对象,即那些如果内存空间足够,我们希望它留着,内存空间不够了,则可以被回收的对象。 jdk1.2之后,引用被分为四种:强引用,软引用,弱引用,虚引用,强度依次减弱。 1. 强引用即代码中普遍存在的如“obj A = new obj()”这类的引用,只要强引用仍然存在,垃圾回收器永远不会回收被引用的对象。 2. 软引用用来描述一些有用但非必需的对象,在系统即将发生内存溢出异常前,会将这些对象列入回收范围内进行第二次回收。如果这次回收没有足够的内存,才会抛出内存溢出异常,可以通过SoftReference类来实现软引用。 3. 弱引用也被用来描述非必需的对象,强度比软引用更弱,只能生存到下一次垃圾收集发生之前,垃圾收集器工作后不管内存空间是否足够都会回收掉这些弱引用关联的对象。可以使用WeekReference来实现弱引用。 4. 虚引用又被称为幽灵引用或者幻影引用,是最弱的一种引用关系。虚引用不会对对象的生存时间产生影响,也无法通过这样的引用来获取对象实例,设置虚引用的唯一目的就是在对象被垃圾回收时收到一个通知。可以通过phantomReference来设置虚引用。

生存还是死亡

要真正宣告一个对象死亡,需要经过一系列过程:

对象自我拯救
对象自我拯救

需要注意的是,finalize()方法只会被系统调用一次,即不可能通过这种方法“拯救”一个对象两次。放入队列之后,会由一个虚拟机自动建立的,低优先级的finalizer线程执行它,这里的执行,仅仅是触发这个方法但不保证等待其运行完成,这是为了防止程序运行缓慢或者崩溃导致整个队列无法继续运行。 此方法很大程度上是对C++程序员的妥协,对应c++中的析构函数。但是此方法代价高昂,无法确定各个对象之间的调用顺序,不确定性大。推荐使用try-final等其他方法。

回收方法区

方法区,即永久代中的垃圾收集是效率低下的,远低于新生代中一次性可以回收70%~95%的空间。永久代中的垃圾收集主要回收两部分内容:废弃常量与无用的类。 废弃常量的回收与java堆中类似,一个字符串如果进入了常量池,但是没有任何一个String对象引用了这个字符串,也没有其他地方引用了这个常量,如果发生了内存回收而且必要的话,此字符串就会被清理。常量池中的其他类(接口),方法,字段的符号引用类似。 而类,需要满足以下三个条件才会被回收: 1. 该类的所有实例已被回收; 2. 加载该类的classLoader已被回收 3. 该类对应的java.lang.class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。 满足以上条件可能被回收。

垃圾收集算法

标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象。 这种算法的不足显而易见:其一效率低,标记和清除两个过程的效率都不高;另一个是空间问题,本算法会产生大量的不连续的内存碎片,空间碎片太多会导致以后在程序运行中需要分配较大的对象时,无法找到足够大的内存空间而不得不提前触发下一次垃圾回收。

复制算法

为了解决标记-清除算法的效率问题,它将内存容量分为相等的两大块,每次只使用其中一块。当一块内存空间用完了,就将还存活的对象复制到另外一块上面。然后再将已使用的那半块内存空间一次性清理掉。 这种算法对整个半区进行内存回收,不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存,实现简单,运行高效。但是二分之一的内存空间无法同时使用,未免太浪费了点。 但是现在的商业虚拟机都采取这种算法,因为新生代(java堆)中的大部分对象的存活时间短,那么就没有必要按照1:1的比例分配内存空间。将内存分为较大的Eden空间与两块较小的survivor空间,hotspot默认Eden空间与survivor空间的比例为8:1,这样只会有10%的空间被浪费。

这里写图片描述
这里写图片描述

但是当survivor空间不够用时,需要依赖其他对象进行分配担保,这对象将直接通过分配担保机制进入老年代。

标记-整理算法

复制收集算法在对象存活率较高的时候会进行较多的复制操作,效率较低,所以在老年代中一般不选用这种算法。 此算法和标记-清理算法类似,但后续过程中不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后清理掉端边界以外的内存。

分代收集算法

此算法并没有什么新鲜思想,根据对象生活周期的不同将内存划分为几块,一般是分为新生代与老年代,可以根据各个年代的特点选择最适当的收集算法。新生代可以采取复制算法,老年代采取标记-清理或标记-清除算法。

HotSpot的算法实现

枚举根节点

可作为根节点的节点主要在全局性的引用与执行上下文中。如果要逐个检查,必然要花费很多时间。此外,可达性分析对时间的敏感还体现在GC停顿上,因为在分析过程中对象关系不可改变否则无法保证准确性,导致GC停顿中必须停顿所有java执行线程。 由于主流java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要检查完所有执行上下文与全局的引用位置,虚拟机可以使用一组叫OopMap的数据结构来达到这个目的,类加载完成式,HotSpot计算出对象什么偏移量对应什么类型,JIT编译过程中,记录下栈与寄存器的引用位置。

安全点(safepoint)

为了节省GC的空间成本,HotSpot没有为每条指令都生成OopMap,只在被称为“安全点”的特殊位置记录了这些信息,即程序在执行时并非在所有地方都能停下来GC,只有在到达安全点时才暂停。安全点的选取以程序是否具备“让程序长时间执行”的特征为标准选定,长时间执行的最明显特征就是指令序列复用,如方法调用,循环跳转,异常跳转等。 对于安全点,另一个问题就是如何让所有线程都执行到安全点上再进行GC,有两种方案可供选择:抢先式中断或主动式中断,抢先式中断即再GC发生时,中断所有线程,如果有线程没有执行到安全点上,就恢复线程,让它再执行到安全点上,现在几乎没有虚拟机采取这种方法。主动式中断即当GC需要中断线程的时候,不直接对线程操作,而简单的设置一个标志,发现中断标志为真时自己中断线程挂起。

安全区域(SafeRegion)

安全区域就是在一段代码片段内,引用关系不会发生变化,在这个片段的任何区域开始GC都是安全的。当线程执行到safeRegion中时,首先标识自己已经进入,那么,当这段时间JVM要发生GC时,就不用管标识自己为safeRegion状态的线程了。当线程要离开此区域时,要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则它就必须等待收到可以安全离开SafeRegion的信号为止。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017年03月11日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 对象“已死”吗?
    • 引用计数分析算法
      • 可达性分析算法
        • 再谈引用
          • 生存还是死亡
            • 回收方法区
            • 垃圾收集算法
              • 标记-清除算法
                • 复制算法
                  • 标记-整理算法
                    • 分代收集算法
                    • HotSpot的算法实现
                      • 枚举根节点
                        • 安全点(safepoint)
                          • 安全区域(SafeRegion)
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档