zoukankan      html  css  js  c++  java
  • 垃圾收集与内存分配

    垃圾回收需要关注的问题:哪些内存需要回收?什么时候回收?如何回收?

    判断内存是否需要回收:

    1、引用计数法:缺点:循环引用,两个对象互相引用,永远无法回收,即便已经应该回收了。

    2、可达性分析算法:通过一系列成为GC Roots的对象作为起始点,从这些节点开始向下搜索,所有所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。其中可以作为GC Roots的对象包括:

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

    再谈引用:

    在JDK1.2之前,Java中的引用的定义是如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,那么这块内存代表着一个引用。这样的方式对于对象的定义就只有被引用和没有引用的两种,但是对于一些内存空间足够的时候可以保存,内存紧张就要删除的对象就没有办法表示。

    在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次减弱。

    强引用是指程序代码种普遍存在的,类似new出来的这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

    软引用是用来描述一些还有用但并非必需的对象。对于软引用的对象,在系统要发生内存溢出异常之前,会列入回收范围进行二次回收。SoftReference。

    弱引用也是非必需对象,弱引用只能保留到下一次垃圾回收之前。WeakReference。

    虚引用也称幽灵引用或者幻影引用。一个对象是否有虚引用不会对它的生存时间构成影响,也无法通过虚引用获取一个对象实例。虚引用的目的就是在这个对象被收集器回收时收到一个系统通知。PhantomReference。

    survive Or Death

    在可达性分析算法种不可达的对象,并非马上要销毁回收,需要宣告一个对象销毁死亡,至少要经历两次标记过程:如果在一次可达性分析后发现没有与GC Roots相连的引用链,它会被第一次标记并且进行一次筛选,筛选的条件时此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或者这个方法已经被调用过了,虚拟机将这两种情况视为没有必要。

    如果判定为必要,对象会被放在F-Queue队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。执行只是触发finalize方法,不会保证执行结束。finalize方法时对象逃脱死亡的最后机会,稍后GC会对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize中拯救自己(重新与引用链上的任何一个对象建立关联,例如把自己的this赋值给某个对象的类变量或者成员变量),那么第二次标记的时候就会被移除这个集合里,如果这次还没有逃逸,就会被真的回收了。

    回收方法区:

    Java虚拟机规范不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的性价比比较低。堆中的新生代一般可以回收70%~95%的空间,而永久代的收集效率远低于此。

    永久代的垃圾回收主要回收两部分:废弃常量和无用的类。

    废弃常量:当前系统的没有任何地方引用了这个常量。

    无用的类:该类的所有实例、类加载器、Class对象没有在任何地方被引用。

    垃圾收集算法:

    标记-清除算法,首先标记所有要回收的对象,标记结束后统一回收所有被标记的对象。缺点:效率问题,标记和清除的效率不高;空间问题,标记清除后会产生大量不连续的空间,碎片过多,大对象申请连续的空间的时候,会提前触发另一次垃圾收集的动作。

    复制算法:

    为了解决效率的问题,使用复制算法。将内容划分为大小相等的两块,每次只使用其中的一块。一块内存用完了,就将还存活的对象复制到另一块上面,然后把已使用的空间一次清理掉。简单高效,但是内存被缩小了一半,成本过高。现在商业虚拟机都采用追踪收集算法回收新生代,IBM公司研究表明,新生代98%对象存活时间很短,不用1:1的比例,而是将内存分为一块较大的Eden空间和两块较小的Survivor空概念,每次使用Eden和其中一块Servivor。当垃圾回收时,将Eden和Survivor空间中还存活的对象一次性的复制到另一块Survivor空间上,最后清理掉Survivor空间。HotSpot默认Eden和Survivor比例时8:1,也就是每次新生代中可用的内存空间时整个新生代容量的90%。如果多余10%存活,需要以来老年代进行分配担保。

    标记-整理算法:

    复制收集算法在对象存活率较高的时候需要进行较多的复制操作,效率比较低。如果使用复制算法不想浪费50%的内存空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有的对象100%存活的极端情况,老年代一般不采用这种算法。

    根据老年代的特点,有人提出了标记整理算法,标记过程和标记清除算法一样,后续步骤采用存活的对象向一端移动,然后清理掉边界外的内存。

    分代收集算法:

    当前的商业虚拟机都采用分代收集算法,根据对象的存活周期将内存分为几块。一般Java堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。新生代每次垃圾收集会有大批对象死去,采用复制算法。老年代存活率高、没有额外空间对它进行分配担保,可以采用标记清理或者标记整理算法回收。

    HotSpot算法

    枚举根节点

    以可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

    另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能保证一致性的快照中进行(这里的一致性指整个分析期间整个系统看起来像冻结在了某个时间点上),这点是导致GC进行时必须停顿所有的的Java执行线程(Sun将这件事称为Stop The World)的其中一个重要原因,即使在号称几乎不会发生停顿的CMS收集器中,枚举根节点时也是要必须进行停顿的。主流Java虚拟机使用的都是准确式GC,所以当系统停顿下来后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应当时有办法直接得知那些地方存放着对象引用。

    安全点

    在OopMap的协助下,HotSpot可以快速准确的完成GC Roots枚举,如果每条指令都生成OopMap会有大量的额外空间。只有在安全点才可以停顿下来GC。安全点的选定是以程序“是否具有让程序长时间执行的特征”为标准选定的,长时间执行比较明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,具有这些功能的执行才会产生Safepoint。

    需要考虑在GC发生时,让所有线程跑到最近的安全点上停下。抢先式中断和主动式中断。

    抢先式中断:当GC发生时,首先让所有线程停顿,如果有线程没有执行到最近的安全点,就恢复线程。(几乎没有虚拟机采用)

    主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动的去轮询这个标志,当发现中断标志为真时就自己中断挂起。

    安全区域

    安全点无法解决一个线程没有分配CPU时间的情况,一个线程如果没有执行就无法到达安全点。安全区是指在一段代码片段中,引用关系不会发生变化。这个区域的任何地方GC都是安全的。线程在执行Safe Region中的代码时,首先标志自己已经进入了Safe Region,当JVM要发起GC,就不用管标志为Safe Region状态的线程了。线程要离开时,会检查系统是否完成了GC Roots枚举(或是整个GC过程),如果完成了,线程继续,否则就要等到可以安全离开安全区的信号为止。

    垃圾收集器(图片来自《深入理解Java虚拟机》)

    Serial收集器

    Serial收集器时最基本、发展历史最悠久的收集器。单线程收集器,进行GC的时候,必须暂停其他所有的工作线程,直到收集结束。Stop The World是由JVM在后台自动发起完成的,在用户不可见的把用户正常的线程全部停掉。(新生代采用复制算法老年代采用标记整理算法)单核下简单高效,Client模式下的虚拟机是个很好的选择。

    ParNew收集器

    是Serial收集器的多线程版本,采用多线程进行垃圾收集。(复制算法)

    Parallel Scavenge

    新生代收集器,也是复制算法,并行多线程。

    目标在于达到一个可控的吞吐量。吞吐量=用户代码时间/(用户代码 + 垃圾收集)。GC停顿短适合于与用户交互的程序,高吞吐量可以高效利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

    Serial Old收集器

    Serial老年代版本,单线程,标记-整理算法。

    Parallel Old收集器

    ParallelScavenge收集器的老年代版本,多线程,标记整理。

    CMS收集器

    CMS(Concurrent Mark Sweep)以获取最短回收停顿时间为目标的收集器。标记清除算法。

    • 初识标记
    • 并发标记
    • 重新标记
    • 并发清除

    其中初识标记和重新标记仍需要Stop The World。

    整个过程耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,总体上看收集器和用户线程基本是并发执行的。

    CMS收集器对于CPU资源很敏感。会抢占处理器资源。

    CMS无法处理浮动垃圾,会出现Concurrent Mode Failure而导致另一次Full GC。由于收集器收集的时候,用户线程还在执行,会导致新的垃圾产生,这些垃圾出现在标记过程之后只能等下次收集,这些垃圾称为浮动垃圾。由于收集垃圾的时候,用户线程还在执行,就需要预留内存给用户线程。当发生Concurrent Mode Failure时,会临时启用Serial Old收集。由于采用的时标记清除,会导致大量碎片,大对象无法分配连续的内存,就会触发Full GC。CMS提供参数供Full GC的时候进行碎片整理和多少次不压缩的Full GC进行碎片整理。

    G1收集器

    面向服务端的垃圾收集器。并行与并发,分代收集(独自管理整个GC堆),空间整合(标记整理),可预测的停顿。

    G1收集器将整个堆分为多个大小相等的独立区域,保留新生代和老年代,但并非完全物理隔离,它们都是一部分Region(不需要连续)的集合。

    G1可以建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,根据收集时间,优先回收价值最大的。使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

    • 初识标记
    • 并发标记
    • 最终标记
    • 筛选回收

    GC日志

    Full GC 代表发生了STW。

    GC发生时间 : GC类型:收集器类型:内存回收情况,GC用时。

    内存分配与回收策略

    对象的内存分配,主要是堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,优先分配在TLAB上。少数情况也会直接分配在老年代。

  • 相关阅读:
    This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.
    vmware 里MAC 鼠标能移动 无法单击
    php获取微信的openid
    PHP 调试打印输出变量
    E0264 Unable to execute '"/usr/bin/codesign" ...'
    PHP 返回JSON
    小米手机安装证书
    CSS3:radial-gradient,径向渐变的使用方法
    CSS3:linear-gradient,线性渐变的使用方法
    CSS3:RGBA的使用方法
  • 原文地址:https://www.cnblogs.com/avalon-merlin/p/9229085.html
Copyright © 2011-2022 走看看