码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2018-07-24 22:49:22      阅读:237      评论:0      收藏:0      [点我收藏+]

标签:pc机   个数   规模   through   分代   引用关系   描述   ref   tab   

目录

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

一、对象存活判断

1.1 对象存活判断算法

  • 引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。 该算法实现简单, 但存在相互引用的问题; (主流的Java虚拟机里面都没有选用引用计数算法来管理内存)
  • 可达性分析法通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下探索,搜索所走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图中,对象object5、object6、object7虽然互有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。    技术分享图片

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

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象; 
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

1.2 java对象四种引用

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

    public class Demo {
        public static void main(String[] args) {
            Object o = new Object();
            System.out.println(o);
            System.gc();//通知JVM的gc进行垃圾回收
            System.out.println(o);
        }
    }
    ----输出:
    java.lang.Object@1540e19d
    java.lang.Object@1540e19d
  2. 软引用(Soft Reference) : 软引用是用来描述一些还有用但并非必需的对象。 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。 如果这次回收还没有足够的内存,才会抛出内存溢出异常。 在JDK 1.2之后,提供了SoftReference类来实 现软引用。

    public class Demo {
        public static void main(String[] args) {
            SoftReference<Object> o = new SoftReference<Object>(new Object());
            System.out.println(o.get());
            System.gc();//通知JVM的gc进行垃圾回收
            System.out.println(o.get());
        }
    }
    --输出:
    java.lang.Object@1540e19d
    java.lang.Object@1540e19d
  3. 弱引用(Weak Reference ) : 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 在JDK 1.2之后,提供了WeakReference类来实现弱引用。

    public class Demo {
        public static void main(String[] args) {
            WeakReference<Object> o = new WeakReference<>(new Object());
            System.out.println(o.get());
            System.gc();//通知JVM的gc进行垃圾回收
            System.out.println(o.get());
        }
    }
    --输出:
    java.lang.Object@1540e19d
    null

    对象已经被回收;

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

    虚引用无法通过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码可以发现 get() 被重写为永远返回 null。

        /**
         * Returns this reference object‘s referent.  Because the referent of a
         * phantom reference is always inaccessible, this method always returns
         * <code>null</code>.
         *
         * @return  <code>null</code>
         */
        public T get() {
            return null;
        }

    那虚引用到底有什么作用?

    其实虚引用主要被用来 跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否 即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue 对象中,从而达到跟踪对象垃圾回收的作用。  所以具体用法和之前两个有所不同,它必须传入一个 ReferenceQueue 对象。当虚引用所引用对象被垃圾回收后,虚引用会被添加到这个队列中。如:

    public class PhantomReferenceTest {
        public static void main(String[] args) {
            ReferenceQueue<String> requeue = new ReferenceQueue<>();
            PhantomReference<String> pr = new PhantomReference<>(new String("T"), requeue);
            System.out.println(pr.get()); // null
            System.gc(); 
            System.runFinalization();
            System.out.println(requeue.poll() == pr); // true
        }
    }

注意点:

  • System.gc(): 告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的;
  • System.runFinalization(): 强制调用已经失去引用的对象的finalize方法;

1.3 对象的生存与死亡判断

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

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queuc的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是如果一个对象在finalize() 方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会, 稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功扬救自己----只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字) 赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合: 如果对象这时候还没有逃脱,那基本上它就真的被回收了。

finalize方法可以用作关闭外部资源之类的工作, 但是代价昂贵, 不确定性大(gc 的时候才可能被执行); 因此尽量避免使用, 关闭资源可以使用try...finally或者其他方式可以做的更好, 更及时;

1.4 回收方法区

很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的, Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

永久代垃圾收集器主要回收两部分内容: 废弃常量 和 无用的类;

  • 废弃常量: 没有任何地方引用这个常量, 在下一次gc时, 如果有必要会被回收
  • 无用的类: 要满足三个条件: 该类所有实例都被回收; 加载给类的ClassLoader已经被回收; 该类对应的java.lang.Class对象没有在任何地方呗引用, 无法在任何地方通过反射获取实例;

二、垃圾收集算法

2.1 标记-清除算法(Mark-Sweep)

分为标记和清除两个阶段: 首先标记所有需要回收的对象, 标记完成后统一回收掉所有被标记的对象;

缺点:

  • 效率低: 标记和清除过程效率不高;
  • 空间不连续: 标记清除会产生大量不连续的内存碎片, 内存碎片过多, 可能会导致之后分配较大对象是无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

2.2 复制算法

将内存按容量分为大小相等的两块, 每次只用其中的一块, 当这一块内存用完, 就将或者的对象复制到另一块上, 然后再把已使用过那块内存空间一次清理掉;

缺点: 内存缩小为原来一般, 内存利用率低; 当对象存活率较高时要执行较多的复制操作, 效率会变低;

现代虚拟机采用复制算法回收新生代, IBM研究表明, 新生代中对象98%是朝生夕死的, 因此可以将内存分为一块较大的Enden空间和两块较小的Survivor空间, 每次使用Enden和其中的一块Survivor; 回收时, 将Enden和Survivor中还存活着的对象一次性拷贝到另一块Survivor空间上, 最后清理掉Enden和刚才用过的Survivor空间. HotSpot中默认 Eden和Survivor大小比例8:1, 可以通过参数-XX:SurvivorRatio=8 设置Enden与Survivor比例;

当Survivor内存不够用时, 会触发老年代内存担保机制;

2.3 标记-整理算法(Mark-Compact)

分为标记和整理两个阶段, 首先标记所有需要清理的对象, 让所有存活的对象想一端移动, 然后直接清理掉端边界以外的内存;

2.4 分代收集算法(Generation Collection)

根据对象存活周期的不同将内存划分为几块; 一般将Java堆分为新生代和老年代:

  • 新生代中每次gc会有大量对象死去, 只有少量存活, 所以选用复制算法;
  • 老年代中对象存活率高, 没有额外空间对它进行分配担保, 使用标记-清除标记-整理

商用虚拟机大多使用分代收集算法;

三、 垃圾收集器

jvm中的七种垃圾收集器: 其中连线了的表示可以相互搭配使用;

技术分享图片

3.1 Serial新生代收集器

新生代收集器

Serial收集器是最基本、发展历史最悠久的收集器。是单线程的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成(stop the world)。

技术分享图片

Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于单个cpu环境由于没有线程交互的开销收集效率较高, Client模式下虚拟机管理的内存,新生代一般几十兆到一两百兆, 停顿时间及时毫秒,可以接受;

### 3.2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The Worl、对象分配规则、回收策略等都与Serial 收集器完全一样。

技术分享图片

ParNew收集器是使用 -XX:+UserConcMarkSweepGC选项后的默认新生代收集器, 也可以使用 -XX:+UseParNewGC选项来强制指定它;

ParNew收集器是许多运行在Server模式下的虚拟机中首选新生代收集器,其中有一个与性能无关但很重要的原因是,除Serial收集器之外,目前只有ParNew它能与CMS收集器配合工作。

并发与并行:

  • 并行是指多个时间同一时刻发生, 并发是多个时间在同一时间段发生

3.3 Parallel Scavenge(并行回收)收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器

该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是

  • 控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数
  • 直接设置吞吐量大小的-XX:GCTimeRatio参数

Parallel Scavenge收集器还有一个参数:-XX:+UseAdaptiveSizePolicy。这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGVPauseMillis参数或GCTimeRation参数给虚拟机设立一个优化目标。

自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别

3.4 Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

如果在Server模式下,主要两大用途:

(1)在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用

(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

Serial Old收集器的工作工程

技术分享图片

3.5 Parallel Old 收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。

技术分享图片

在注重吞吐量 及CPU资源敏感的场合, 可以优先考虑Parallel Scavenge 加 Parallel Old收集器组合;

3.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:

(1)初始标记: 仅仅只是标记GC Roots能直接关联到的对象,速度快

(2)并发标记: 进行GC Roots Tracing(追踪)的过程, 耗时较长

(3)重新标记: 为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录; 耗时比初始标记稍长, 但远小于并发标记 ;

(4)并发清除: gc线程与用户线程并发执行, 由于用户线程并发执行, 会出现新的垃圾, 新的垃圾(称之为浮动垃圾)需要到下一次gc才会被回收掉;

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”.

技术分享图片

CMS收集器主要优点:并发收集,低停顿。

CMS三个明显的缺点:

(1)CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种。所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想

(2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阀值已经提升至92%。

(3)CMS是基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间变长了。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标识每次进入Full GC时都进行碎片整理)

3.7 G1(Garbage First)收集器

基于标记-整理算法实现的收集器, 因此它不会产生空间碎片;

G1收集器的优势:

(1)并行与并发

(2)分代收集

(3)空间整理 (标记整理算法,复制算法)

(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)

使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。

G1收集器之所以能建立可预测的停顿时间模型,**是因为它可以有计划地避免在真个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的又来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽量可能高的收集效率;

G1 内存“化整为零”的思路:
Regin之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用, 虚拟机都是使用Remembered Set来避免全堆扫描; G1中每个Region都有一个与之对应的Remember Set, 虚拟机发现程序在Reference类型的数据进行写操作时, 会产生一个Write Barrier暂时中断写操作, 检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中对象引用了新生代对象), 如果是, 便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。 当进行内存回收时, 在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏;

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:

  1. 初始标记: 标记GC Roots能直接关联到的对象, 并且修改TAMS(Next Top at Mark Start)的值, 这阶段需要停顿线程, 但耗时很短;

  2. 并发标记: 从GC Roots开始对堆中对象进行可达性分析, 找到存活对象, 耗时较长,但可以与用户线程并发执行;

  3. 最终标记:为了修正在并发标记期间因用户程序继续操作而导致标记产生变动的那一部分记录, 虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面, 最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中, 这阶段需要停顿线程, 但是可并行执行;

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

技术分享图片

四、 内存分配与回收策略

java自动内存管理可以最终归结为自动化解决两个问题:

  • 给对象分配内存
  • 回收分配给对象的内存

4.1 对象有限在Eden区分配

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

  • 新生代GC(Minor GC): 指发生在新生代的垃圾收集动作, 应为Java对象大多都是具备朝生夕死的特性, 所以Minor GC 非常频繁, 一般回收速度也比较块
  • 老年代GC(Major GC / Full GC): 指 发生在老年代的GC, 出现了Major GC, 经常会伴随至少一次的Minor GC(但非绝对的, 在ParallelScavenge 收集器的收集策略里就有直接进行Major GC的策略选择过程); Major GC速度一般比Monor GC慢10倍以上;

4.2 大对象直接进入老年代

所谓大对象就是指, 需要大量连续内存空间的Java对象, 最典型的大对象就是那种很长的字符串及数组; 虚拟机提供一个 -XX:PretenureSizeThreshold参数, 令大于这个设置的对象直接在老年代中分配; 这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝;

注意

PretenureSizeThreshold 参数只对Serial 和ParNew两个收集器有效;Parallel Scanvenge收集器不认识这个参数, Parallel Scanvenge收集器一般并不需要设置; 如果遇到必须使用该参数的场合, 可以考虑使用ParNew 和 CMS的收集器组合;

4.3 长期存活的对象直接进入老年代

虚拟机给每个对象定义了一个年龄(Age)计数器, 如果对象在Eden出生并经历过第一次Minor GC后仍然存活, 并且能够被Survivor容纳的话, 将会被移动到Survivor空间中, 并将对象年两设置为1, 以后对象在Survivor区每熬过一次Minor GC, 年龄增加1岁, 当他的年龄到达一定程度(默认为15岁)时, 就会被晋升到老年代中; 对象晋升到老年代的年龄阈值, 可以通过参数

-XX:MaxTenuringThreshold来设置;

4.4 动态年龄判断

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

4.5 空间分配担保

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

HandlePromotionFailure在JDK1.6024之后失效

空间分配担保变为: 在发生Minor GC之前,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

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

标签:pc机   个数   规模   through   分代   引用关系   描述   ref   tab   

原文地址:https://www.cnblogs.com/jxkun/p/9363071.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!