zoukankan      html  css  js  c++  java
  • JVM学习笔记(二)——垃圾收集器和内存分配策略

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙内的人却想出去。
    垃圾收集器和内存分配策略

    对于Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域的内存随线程而生,随线程而灭。所以方法或者线程结束的时候,内存就自然的随着回收了。而Java堆和方法区的内存则不一样,这些部分的内存是需要动态分配的。

    栈中的栈帧随着方法的进入和退出而有条不紊地执行着入栈和出栈的操作。每一个线栈中分配多少内存基本上是在类结构确定下来时就已知的。

    Java堆和方法区中,一个接口的多个实现类、一个方法中的多个分支也可能不一样,我们只有在程序处于运行期间才知道会创建那些对象。

    一、如何判断对象已死

    1.1 引用计数法

    在一个对象被引用时加一,被去除引用时减一,这样我们就可以通过判断引用计数是否为零来判断一个对象是垃圾。

    优点是实现简单,判定效率也很高,但很少被使用,因为存在着一个很致命的问题,即循环引用问题

    A引用了B,B引用了C,C引用了A,它们各自的引用计数都为 1。但是它们三个对象却从未被其他对象引用,只有它们自身互相引用。从垃圾的判断思想来看,它们三个确实是不被其他对象引用的,但是此时它们的引用计数却不为零。这就是引用计数法存在的循环引用问题。




    1.2 可达性分析算法(GC Root Tracing)

    从GC Root出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾

    • GC Root:

      • 虚拟机栈中(栈帧中的本地变量表)中引用的对象
      • 方法区中静态属性引用的对象
      • 方法区中常量引用的对象

    简单地说,GC Root就是经过精心挑选的一组活跃引用,这些引用是肯定存活的。那么通过这些引用延伸到的对象,自然也是存活的。

    此时需要注意的是,即使是在可达性分析算法中不可达的对象,也并非是“非死不可”,此时它们处于“缓刑”阶段,然后需要至少经历两次标记过程:

    1. 此对象是否有必要执行finalize()方法,如果有必要则进入下一阶段。

      当对象没有覆盖finalize()方法或者此方法已经被虚拟机调用过,则会被视为没必要执行。如果有必要执行,则会被放置在F-QueueL的一个队列中,并由一个低优先级的Finalizer线程去执行它

    2. F-QueueL中的对象进行二次标记,如果对象在finalize()中成功拯救自己,则会被移出“即将回收”的集合。

      finalize():只要重新与引用链上任意一个对象建立关联即可

    再谈引用:

    • 强引用:在程序代码之中普遍存在的,类似Object c=new Object()
    • 软引用:有用但并非有必需的对象,对于这一类对象,在系统将要发生内存溢出的之前,将会把这些对象列进回收范围之中进行第二次回收
    • 弱引用:非必需对象只能生存到下一次垃圾收集之前
    • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例,一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,JDK1.2以后通过PhantomReference类来实现虚引用



    1.3 方法区的回收

    虽然Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,但方法区并不是没有垃圾回收


    方法区(永久代)的垃圾回收主要是两部分的内容:

    仅仅是“可以”,并不是像对象一样,不使用了就必然会被回收

    • 废弃常量

      例如一个字符串“abc"已经进入了常量池,但当前系统并没有任何一个String对象的值引用常量池中的“abc”变量的

    • 无用的类

      该类的所有实例都已经被回收

      加载该类的ClassLoader已经被回收

      该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。(以上三点需同时满足)






    二、垃圾回收算法

    2.1 标记-清除算法

    算法分为两个部分:

    • 标记:首先标记出所有需要回收的对象
    • 清除:在标记完成后统一回收所有被标记的对象

    不足:

    • 效率问题

    标记和清除的效率都很低

    • 空间问题

    标记清除之后会产生大量不连续的内存碎片,导致以后分配较大的对象时,找不到足够多的连续内存而不得不触发一次GC




    2.2 复制算法

    将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象,之后交换两个内存块的角色,完成垃圾回收

    不足:

    • 要将内存空间折半,极大地浪费了内存空间。

    改进:

    • IBM公司的专门研究表明,新生代的98%对象都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden空间和Survivor中还活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚刚用过的那一个Survivor。
    • HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1:1,如果Survivor空间不够用时,则需要依赖其他内存空间进行担保(直接进入老年代)



    2.3 标记整理算法

    可以理解是标记清除算法的优化版,其同样需要经历两个阶段:

    • 标记结算

    从 GC Root 引用集合触发去标记所有对象

    • 整理阶段

    让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存






    三、分代思想

    如果我们单独采用任何一种算法,那么最终的垃圾回收效率都不会很好。因此在实际的垃圾回收算法中采用了分代算法。即根据对象存活周期的不同将内存划分为几块。一般是将堆划分为新生代老年代,这样就可以根据各个年代的特点采用不同的垃圾回收算法。

    例如对于存活对象少的新生代区域,比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。而对于老年代这种存活对象多的区域,比较适合采用标记压缩算法或标记清除算法,这样不需要移动太多的内存对象。

    HotSpot的算法实现

    枚举根节点

    可达性分析从GC Roots结点找引用链,但可作为结点的全局性引用和执行上下文的数据可能会过于庞大,因此如果要逐个检查里面的应用,那么必然会消耗非常多的资源。此外可达性分析对于时间的敏感性还体现在GC停顿上,不可以出现分析对象引用关系的过程时其还在不断变化,因此GC进行时必须停顿所有Java执行线程(“Stop The World”)。

    目前主流的Java虚拟机使用的都是准确式GC:

    所以当执行系统停顿下来时,并不需要逐个检查所有全局性引用和执行上下文。HotSpot使用一组称为OopMap的数据结构来达到这个目的。

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


    安全点

    在OopMap的帮助下,HotSop可以很快的完成GC Roots枚举,但是如果为每一条指令都生成对于的OopMap,那么会使得GC的空间成本很高。

    所以程序只是在特点的位置(安全点)停下来开始GC

    安全点的选择不能选得太少让GC等待时间过长,也不能选得太多让程序长时间执行。所以选择的地方的特征是指令序列复用,例如方法调用、循环跳转、异常跳转等。

    如何在GC发生是让所有线程都"跑"到最近的安全点?

    1. 抢先式中断

    不需要线程的执行代码主动去配合,首先把所有线程中断,然后让没有到安全点的线程,跑到安全点上

    1. 主动式中断

    不直接对线程操作,仅仅简单地设置一个标志,各个线程执行是主动轮询这个标志


    安全区域

    如果在程序线程处于Sleep状态或者Blocked状态时,线程无法响应JVM中断请求,走到安全点去挂起。对于这种情况就需要安全区域来解决

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






    四、分区思想

    分代思想按照对象的生命周期长短将其分为了两个部分(新生代、老年代),但 JVM 中其实还有一个分区思想,即将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个区间,可以较好地控制 GC 时间。






    五、垃圾回收器

    5.1 串形(Serial)回收器

    串行回收器是指使用单线程进行垃圾回收的回收器。因为每次回收时只有一个线程,因此串行回收器在并发能力较弱的计算机上,其专注性和独占性的特点往往能让其有更好的性能表现。

    串行回收器可以在新生代和老年代使用,根据作用于不同的堆空间,分为新生代串行回收器和老年代串行回收器。

    5.1.1 新生代串行回收器

    在新生代串行回收器中使用的是复制算法。在串行回收器进行垃圾回收时,会触发Stop-The-World现象,即其他线程都需要暂停,等待垃圾回收完成。因此在某些情况下,其会造成较为糟糕的用户体验。


    5.1.2 老年代串行回收器

    在老年代串行回收器中使用的是标记整理算法。其与新生代串行收集器一样,只能串行、独占式地进行垃圾回收,因此也经常会有较长时间的 Stop-The-World 发生。

    但老年代串行回收器的好处之一,就是其可以与多种新生代回收器配合使用




    5.2 并行回收器

    5.2.1 新生代ParNew回收器

    串行回收器的多线程版本,其回收策略、算法以及参数和新生代串行回收器一样。

    新生代 ParNew 回收器同样使用复制算法的垃圾回收算法,其垃圾收集过程中同样会触发Stop-The-World现象。但因为其使用多线程进行垃圾回收,因此在并发能力强的CPU上,其产生的停顿时间要短于串行回收器

    但在单CPU或并能能力弱的系统中,并行回收器效果会因为线程切换的原因,其实际表现反而不如串行回收器


    5.2.2 新生代Parallel GC回收器

    新生代 Parallel GC 回收器与新生代 ParNew 回收器非常类似,其也是使用复制算法,都是多线程、独占式的收集器,也会导致 Stop-The-World。但其余 ParNew 回收器的一个重大不同是:其非常注重系统的吞吐量

    之所以说新生代 Parallel GC 回收器非常注重系统吞吐量,是因为其有一个自适应GC调节策略


    5.2.3 老年代 ParallelOldGC 回收器

    老年代 ParallelOldGC 回收器也是一种多线程并发的回收器,与新生代ParallelGC收集器一样,其也是注重吞吐量的收集器,只不过其是作用于老年代

    ParallelOldGC回收器使用的是标记整理算法,只有在 JDK1.6中才可以使用。




    5.3 CMS回收器

    CMS 回收器主要关注系统停顿时间。CMS回收器全称为 Concurrent Mark Sweep,意为标记清除算法,其是一个使用多线程并行回收的垃圾回收器。

    CMS 的主要工作步骤有:

    1. 初始标记
    2. 并发标记
    3. 重新标记

    为了修正标记期间因程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记阶段时间较长,但远小于并发标记

    1. 并发清除

    耗时最长的并发标记和并发清除线程都可以与用户线程并发执行

    优点:

    • 并发收集低停顿

    缺点:

    • 对CPU资源非常敏感
    • 无法处理浮动资源

    由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,只能等待下一次GC,这些垃圾就被称为浮动垃圾

    • 会出现空间碎片

    基于标记-清理算法








    5.4 G1收集器

    G1 回收器是 JDK 1.7 中使用的全新垃圾回收器,从长期目标来看,其是为了取代 CMS 回收器。

    G1 回收器拥有独特的垃圾回收策略,和之前所有垃圾回收器采用的垃圾回收策略不同。从分代看,G1 依然属于分代垃圾回收器。但它最大的改变是使用了分区算法,从而使得 Eden 区、From 区、Survivor 区和老年代等各块内存不必连续

    工作过程:

    1. 初始标记

    耗时较短,仅仅标记一下GC Roots能够直接关联到的对象

    1. 并发标记

    耗时较长,进行可达性分析,找出存活的对象

    1. 最终标记

    修正在并发标记过程中因用户程序继续运行而导致的标记变动的那一部分

    1. 筛选回收

    首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划

    特点:

    • 并行和并发
    • 分代收集
    • 空间整合

    基于标记-整理算法

    • 可预测的停顿

    通过建立一个可预测的停顿时间模型,使得消耗在垃圾收集上的时间不得超过可预测的时间。之所以能够建立一个可预测的停顿时间模型,是因为它在后台维护了一个优先列表,每次优先收集价值最大的Region,有效地提高收集效率。






    六、内存分配和回收策略

    Java对象的内存分配,大方向来讲,就是在堆上分配(但也有可能经过JIT编译后被拆散为标量类型并间接地在栈上分配)

    对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按照线程优先在TLAB上分配,少数情况下也会直接在老年代中分配,分配规则并不是固定不变的,其细节取决于使用的哪一种垃圾回收器组合以及虚拟机的相关参数设置。

    以下是几条最普遍的内存分配规则:

    6.1 对象优先在Eden分配

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


    6.2 大对象直接进入老年代

    大对象是指需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串或者数组


    6.3 长期存活的对象将进入老年代

    虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一此Minor GC后仍然存错,并且被Survivor容纳的话,就将对象年龄设为1,此后每经历过一此Minor GC,对象年龄就加1,当年龄增加到一定程度时(默认为15),就晋升到老年代


    6.4 动态对象年龄判定

    虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代


    6.5 空间担保分配

    虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么可以确认Minor GC是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败?如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行一次Minor GC,尽管这次是有风险的。如果小于或者HandlePromotionFailure设置不允许冒险,那这是也要改为进行一次Full GC。

    什么是冒险?前面提到过新生代使用复制收集算法,但为了提升内存利用率。只使用其中一个survivor空间作为备份。因此当出现大量对象在Minor GC后仍然存活的情况。就需要老年代进行分担,把survivor无法容纳的对象直接进入老年代。前提是老年代本身还有容纳这些对象的剩余空间。有多少对象会活下来,在实际完成内存回收之前是无法明确知道。所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值与剩余老年代的剩余空间进行比较。决定是否进行一次Full GC让老年代腾出更多空间。

    如果担保失败的话,就只好在担保失败后重新发起一次Full GC.






    七、垃圾回收的几种类型

    7.1 Minor GC

    新生代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。对于 Minor GC,你需要知道的一些点:

    • JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以 Eden 区越小,越频繁执行 Minor GC。

    • 当年轻代中的 Eden 区分配满的时候,年轻代中的部分对象会晋升到老年代,所以 Minor GC 后老年代的占用量通常会有所升高。

    • 质疑常规的认知,所有的 Minor GC 都会触发 Stop-The-World,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的,因为大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果情况相反,即 Eden 区大部分新生对象不符合 GC 条件(即他们不被垃圾回收器收集),那么 Minor GC 执行时暂停的时间将会长很多(因为他们要JVM要将他们复制到 Survivor 区或老年代)。


    7.2 Major GC

    从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。

    许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。

    Minor GC 作用于新生代,Major GC 作用于老年代。 分配对象内存时发现内存不够,触发 Minor GC。Minor GC 会将对象移到老年代中,如果此时老年代空间不够,那么触发 Major GC。因此才会说,许多 Major GC 是由 Minor GC 引起的。


    7.3 Full GC

    Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(如果有的话)。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。

    当准备要触发一次 Minor GC 时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC 而是转为触发 Full GC。因为JVM此时认为:之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那么我就直接帮你把事情给做了吧,直接来一次 Full GC,整理一下老年代和年轻代的空间。

    另外,即在永久代分配空间但已经没有足够空间时,也会触发 Full GC。

    7.4 Stop-The-World

    Stop-The-World,中文一般翻译为全世界暂停,是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。

    在 Stop-The-World 这段时间里,所有非垃圾回收线程都无法工作,都暂停下来。只有等到垃圾回收线程工作完成才可以继续工作。可以看出,Stop-The-World 时间的长短将关系到应用程序的响应时间,因此在 GC 过程中,Stop-The-World 的时间是一个非常重要的指标。






    推荐文章:

    陈树义 JVM基础系列第8讲:JVM 垃圾回收机制

    陈树义 JVM基础系列第9讲:JVM垃圾回收器

    陈树义 JVM基础系列第10讲:垃圾回收的几种类型



    参考

    [1]周志明.深入理解Java虚拟机.北京:机械工业出版社
    [2]https://www.cnblogs.com/chanshuyi/p/jvm_serial_08_jvm_garbage_collection.html
    [3]https://www.cnblogs.com/chanshuyi/p/jvm_serial_09_jvm_garabage_collector.html
    [4]https://www.cnblogs.com/chanshuyi/p/jvm_serial_10_gc_type.html

  • 相关阅读:
    Unity3D-实现连续点击两次返回键退出游戏(安卓/IOS)
    Unity3D插件-自制小插件、简化代码便于使用(新手至高手进阶必经之路)
    Unity3D安卓程序中提示窗与常用静态方法封装
    Unity插件-NGUI使用教程
    Unity 3D游戏-塔防类游戏源码:重要方法和功能的实现
    Unity 3D编辑器扩展介绍、教程(二) —— 创建窗口
    Unity 3D编辑器扩展介绍、教程(一) —— 创建菜单项
    Unity 3D换装系统教程/Demo
    Unity3D游戏-愤怒的小鸟游戏源码和教程(二)
    WebApi接口测试
  • 原文地址:https://www.cnblogs.com/ccink/p/13362433.html
Copyright © 2011-2022 走看看