• 2.2 自动内存管理机制--垃圾收集器与内存分配策略


    自动内存管理机制

    第三章 垃圾收集器与内存分配策略

    【哪些内存需要回收?、什么时候回收?如何回收?】


     一、对象已死吗——对象是否是垃圾?

    1.引用计数算法

      【基本思想】:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加一;当引用失效时,计数器值减一;任何时刻计数器值为0的对象就是不可能再被使用的。

      实现简单、判别效率高。但是很难解决对象之间相互循环引用的问题(计数器值永不为0,一直占用内存),每次对象赋值时都要维护计数器。

    2.可达性分析算法

      【基本思想】:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,则证明这个对象是不可用的。 

       在Java语言中,可作为GC Roots对象包括以下几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
    • 方法区中类静态属性引用的对象;
    • 方法区中常量引用的对象;
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象;
      总结就是,方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。

    3、引用

      【强引用】就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

      【软引用】是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

      【弱引用】也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

      【虚引用】它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

    4、finalize()

      真正宣告一个对象死亡,至少需要两次标记过程:

        ①可达性分析,GC Roots链不可达。

        ②该对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过。

      如果一个对象被判定有必要执行finalize()方法,那么这个对象会被放置在F-Queue队列中,并且会被由虚拟机创建的Finalizer线程所执行。如果对想要在finalize()拯救自己,只需要重新与引用链上任何一个对象建立关联即可。

      任何一个对象的finalize()方法只会被系统自动调用一次。这个方法不推荐使用,代价高,精度低。

    5、方法区回收

      方法区(或者HotSpot虚拟机中的永久代)垃圾回收性价比低,在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%-95%空间,而永久代效率远低于此。永久代的垃圾收集主要分为两个部分:废弃常量无用的类

      【废弃常量】:如果一个常量池中的常量没有被任何地方所引用,如果有必要的话,可以进行回收。

      【无用的】:同时满足以下三个条件才能称为无用的类:①此类的所有实例都已经被回收,也就是说Java堆中没有任何该类的实例。 ②加载该类的ClassLoader都已经被回收 。③ 对应的Class对象没有在任何地方被引用,无法通过反射访问该类的方法。


     二、垃圾收集算法——怎么回收垃圾

    1.标记-清除算法

      【算法思想】:分为标记、清除两个阶段。首先标记出所有需要回收的对象(标记方法可参考上述引用计数算法和可达性分析算法),标记完成后统一回收所有被标记的对象。

      效率不高且产生大量内存空间碎片。

    2.复制算法

      【算法思想】:将可用内存分为容量大小相同的两块,每次只使用其中的一块。当着一块内存使用完之后,就将还存活的对象复制到另一块上面,然后将一使用过的内存空间一次清除。

      可用内存要变为原来的二分之一,不实用。

      现代虚拟机都采用这种收集算法来回收新生代,新生代存活率低,不用一比一分配。

    3.标记-整理算法

      【算法思想】:是标记-清除的升级版,也是先标记,然后将所有成活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    4.分代收集算法

      【算法思想】:集大成者。一般将Java堆分为新生代和老年代,对于新生代存活率低,使用复制算法。对于老年代,存活率高,使用标记-整理算法。


    三、HotSpot的算法实现——什么时候发起内存回收

    触发GC运行的条件要分新生代和老年代的情况来进行讨论,有以下几点会触发GC:

     Minor GC

      当Eden区满时;

      【刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次(15岁),该对象就会被送到老年代中。】

      【为什么需要survivor:如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。】

      【为什么是两个survivor区域:内存碎片】

     Full GC

      调用System.gc时,系统建议执行Full GC,但是不必然执行

      老年代空间不足

      方法区空间不足

      通过Minor GC后进入老年代的平均大小大于老年代的可用内存

      由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小


    四、垃圾收集器

    1.Serial收集器

      是一个单线程的收集器。进行垃圾收集时,必须暂停其他所有的工作线程,知道它收集结束。

    采用复制算法,针对新生代。

      简单高效,对于限定单个CPU的环境来说,没有线程交互的开销。Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

       "-XX:+UseSerialGC":添加该参数来显式的使用串行垃圾收集器;

    2.ParNew收集器

      ParNew收集器是Serial收集器的并行多线程版本,除了多线程之外其他的和Serial相同。

      适合于运行在Server模式下的虚拟机的新生代的收集器,除了Serial收集器以外,只有他才能和CMS收集器配合工作。但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

      "-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;

          "-XX:+UseParNewGC":强制指定使用ParNew;    

          "-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

      并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。(同一时间点)

      并发:指用户线程与垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序在另一个CPU上运行。(同一时间段)

    3.Parallel Scavenge收集器

      是一个新生代的收集器,使用复制算法,并且是并行的多线程收集器。

      Parallel Scavenge收集器关注点是达到一个可控制的吞吐量,即减少垃圾收集时间,让用户活得更长的运行时间。吞吐量=运行用户程序时间/(运行用户程序时间+垃圾收集时间)。

    4.Serial Old收集器

      Serial Old收集器是一个老年代单线程收集器,使用标记-整理算法

      该收集器主要意义是用在Client模式下的虚拟机使用。

      如果是Server模式下,那么有两大用途:

        ①与Parallel Scavenge收集器配合使用。

        ②作为CMS收集器的后备预案。

    5.Parallel Old收集器

      Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程标记-整理算法。

      在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge+Parallel Old组合。

    6.CMS收集器 详细过程

    https://blog.csdn.net/Mark__Zeng/article/details/48751053?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

      CMS收集器是一种以获取最短回收停顿时间为目标的收集器,使用标记-清除算法,适合于与用户交互较多的场景。

      【优点】:并发收集、低停顿

      【缺点】:①对CPU资源敏感(线程交替运行频繁);②无法处理浮动垃圾(产生于标记过程之后的垃圾);③会产生内存碎片。

      【运行过程】:分为初始标记并发标记重新标记并发清理四个过程。其中初始标记和重新标记需要Stop The World。

      ①初始标记:仅仅是标记一下GC Roots能直接关联到的对象。

      ②并发标记:就是进行GC RootsTracing的过程,刚才产生的集合中标记处存活对象。

      ③重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

      ④并发清除:垃圾收集。

    优点:
      1).将stop-the-world的时间降到最低,能给电商网站用户带来最好的体验。                
      2).尽管CMS的GC线程对CPU的占用率会比较高,但在多核的服务器上还是展现了优越的特性,目前也被部署在国内的各大电商网站上。
    缺点:
      1).对CMS在单核和多核机器上做测试。发现CMS在收集过程中会大量占用CPU的时间。所以在第二个阶段会比较漫长,所以一般将其设置在多核机器上。并且对于CMS在单核机器上的表现设计了一套启发式控制。这种控制将收集器看作一个掠夺者,而收集器会尽量赶在用户线程分配新的对象之前完成收集的工作。同样也有可能会出现用户线程希望分配对象,但目前空间不够,则需要停下收集器,这样会让整个收集时间大大加长。所以这时候一搬会选择扩张堆的大小。
      2).标记清除算法一直令人诟病的内存碎片问题,造成了堆空间的浪费以及利用率的下降。
      3).需要较大的内存空间去运行,因为在很多并行的阶段,要考虑到用户程序运行时也要分配空间。所以一般选择在堆利用率达到一个常数的时候就开启CMS的收集。可以在VM argument里来设置这个阀值。(–XX:CMSInitiatingOccupancyFraction =n,n=0~100)
      4).会产生浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,伴随程序自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好等到下一次GC去处理。

    7.G1收集器 详细过程

    https://www.jianshu.com/p/0f1f5adffdc1

    7.1 分区存储

       当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。

    7.2 Rset  Cset

      Remember Set:记录其他Region引用当前Region的一条记录(每个region都维护)。

      Collection Set:本次GC需要清理的Region集合。

    7.3 垃圾回收过程

    7.3.1 Young GC

      和其他收集器类似,只不过把未标识的Region当作Survivor to区域。复制算法。

     7.3.2 MixGC 

      没有针对Old区域的GC,因为Old GC时也发生YoungGC。

      1. 初始标记,标记GC Root直接引用对象和该Region(称为RootRegion).STW

      2. 遍历整个Old区域,标记Rset有RootRegion的所有Region。

      3. 并发标记,和CMS相同,只不过遍历范围缩小,只遍历刚才Old被标记的region。

      4. 重新标记,和CMS相同,只不过使用SATB。效率更高。STW

      5. 清理,Cset是选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region,并不是把所有Old区域的垃圾全都清除。STW

    7.3.3 Full GC

      如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.

    7.4 SATB

    它是根据三色标记算法,把对象设置为了三种状态:

    • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
    • 灰: 对象被标记了,但是它的field还没有被标记或标记完
    • 黑: 对象被标记了,并且它的所有field也被标记完了。
      垃圾收集器的工作过程,就是通过灰色对象的指针扫描它指向的白色对象,如果找到一个白色对象,就将它设置为灰色,如果某个灰色对象的可达对象已经全部找完,就将它设置为黑色对象。当在当前集合中找不到灰色的对象时,就说明该集合的回收动作完成,然后所有白色的对象的都会被回收。


      G1收集器是一款面向服务端的垃圾收集器。与其他垃圾收集器相比,具备以下特点:

       ①并行和并发:

       ②分代收集:可独立完成整个垃圾收集过程,但也是采用不同收集方法。

       ③空间整合:从整体上看是标记管理算法实现的,从局部(两个Region)上看是基于复制算法实现的,没有空间碎片。

       ④可预测的停顿:除了追求低停顿之外,还能建立可预测的停顿时间模型。

     8. https://mp.weixin.qq.com/s/ag5u2EPObx7bZr7hkcrOTg


    五、内存分配策略

      对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

     

    1.对象优先在Eden分配

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

      新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多朝生夕灭,所以Minor GC非常频繁。

      老年代GC(Major GC/Full GC):指发生在老年代的GC。

    2.大对象直接进入老年代

      大对象是指需要大量连续内存空间的Java对象,例如长字符串和数组。

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

      虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象每在Survivor空间中经历一次Minior GC,年龄就增加一岁,增加到一定程度就会晋升到老年代。

    4.动态对象年龄判定

      为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。例如 3 7 7 7 7 7 8 9=》7的个数大于一半,则7 8 9 进入老年代。

    5.空间分配担保

      保证老年代有足够空间迎接新成员(经过Minor GC的对象)。

    6. 对象一定会分配在堆里吗?——逃逸分析

      如果一个对象只作用于一个方法内,那么该对象会分配到栈中,减少GC压力。

    https://www.cnblogs.com/jackeason/p/11336374.html

  • 相关阅读:
    try? try! try do catch try 使用详解
    Swift Write to file 到电脑桌面
    NSLayoutConstraint 使用详解 VFL使用介绍
    automaticallyAdjustsScrollViewInsets 详解
    Swift 给UITableView 写extension 时 报错 does not conform to protocol 'UITableViewDataSource'
    OC Swift中检查代码行数
    Swift中 @objc 使用介绍
    SWift中 '?' must be followed by a call, member lookup, or subscript 错误解决方案
    Swift 中 insetBy(dx: CGFloat, dy: CGFloat) -> CGRect 用法详解
    求1000之内所有“完数”(注:C程序设计(第四版) 谭浩强/著 P141-9)
  • 原文地址:https://www.cnblogs.com/qmillet/p/12041289.html
Copyright © 2020-2023  润新知