Java虚拟机之垃圾收集器与内存分配策略

为什么要了解GC和内存分配?答案很简单:当需要排查各种内存溢出,内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对这些"自动化"的技术实施必要的监控和调节。

Java堆中一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

判断对象是否存活的算法:1.引用计数算法。缺点:很难解决对象之间相互循环引用的问题; 2.可达性算法(Java虚拟机采用),在Java语言中,可作为GC Roots的对象包括下面几种: 1.虚拟机栈(栈帧中的本地变量表)中引用的对象 ; 2.方法区中类静态属性引用的对象;3方法区中常量引用的对象;4.本地方法栈中JNI(即一般说的Native方法)引用的对象

 

再谈引用

在JDK1.2之后,Java地引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用四种,这四种强度依次逐渐减弱

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

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

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

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

 

回收方法区(HotSpot虚拟机中的永久代)

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

回收常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,例如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做"abc"的,换句话说,就是没有任何String对象引用常量池中的"abc"常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个"abc"常量就会被系统清理出常量池。常量池中的其他类(接口),方法,字段的符号引用也与此类似。

判断一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2.加载该类的ClassLoader已经被回收。3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用就必然会回收,是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+/TraceClassLoading,-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。

  在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

 

垃圾收集算法

1.标记-清除算法(Mark-Sweep)

 

                                              标记-清除算法示意图

2.复制算法(Copying)

现在的商业虚拟机都采用这种收集算法来回收新生代

                            复制算法示意图

3.标记-整理算法

                                标记-整理算法示意图

4.分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法

例如新生代可采用复制算法,而老年代可采用“标记-清理”或者“标记-整理”算法来进行回收。

 

垃圾收集器

垃圾收集器是内存回收的具体实现。这里讨论的收集器是基于JDK1.7Update 14之后的HotSpot虚拟机(其中正式提供了商用的G1收集器)

HotSpot虚拟机的垃圾收集器

如果两个收集器间有连线,说明它们可以搭配使用

 

Serial收集器

单线程收集器。不仅只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,知道它收集结束。

是虚拟机运行在Client模式下的默认新生代收集器。对于运行在Client模式下的虚拟机来说是一个很好的选择。

 

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所控制参数(例如:-XX;SurvivorRatio,-XX:PretenureSizeThreshold,-XX:HandlePromotionFailure等),收集算法,Stop The World,对象分配规则,回收策略等都与Serial收集器完全一样。

是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它和Serial收集器能与CMS收集器(并发收集器)配合工作。使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,或使用-XX:UsePaeNewGC选项来强制指定它。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

Parallel Scavenge收集器(新生代收集器)

使用复制算法,并行(只是指垃圾收集线程可并行,此时用户线程处于等待状态)的多线程收集器。目标:达到一个可控制的吞吐量。吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)停顿时间越短越适合需要与用户交互的程序;高吞吐量则可以高效率地利用CPU时间,尽快完成程序运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的_XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio。参数-XX:+UseAdaptiveSizePolicy是一个开关参数,打开后,不需要手工设置指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GCGC自适应的调节策略。

 

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,是一个单线程收集器,使用 标记-整理”算法。主要意义在于给Client模式下的虚拟机使用。如果在Server模式下,主要有两大用途;1.在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,2.作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

 

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。JDK1.6中开始提供。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器

 

CMS(Concurrent Mark Sweep)收集器

以一种获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现

运作过程分为4个步骤;1.初始标记。2.并发标记。3.重新标记。4.并发清除。

CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

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

缺点:1.CMS收集器对CPU资源非常敏感。 2.CMS收集器无法处理浮动垃圾(出现在标记之后,CMS无法在当次收集中处理掉它们,只能下次GC时清理,导致CMS运行期间预留的内存无法满足程序需要),可能出现“Concurrent Mode Failure”失败则导致另一次Full GC的产生。此时可能启动后备预案Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间更长。所以说参数-XX:CMSInitiatingOccupancyFraction设置得很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。3.会产生空间碎片,导致不得不提前触发一次Full GC.为此,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并并整理过程,内存整理的过程是无法并发的,导致停顿时间变长。参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)

 

G1收集器

是一款面向服务端应用的垃圾收集器。

优点:1.并行与并发。通过并发的方式让Java程序继续执行。2.分代收集。3.空间整合。整体基于“标记-整理”算法,局部(两个Region之间)基于“复制”算法实现。  3.可预测的停顿。

没有了新生代和老年代的划分,代之以Region做全局划分。每个Region都有一个与之对应的Remembered Set

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

1.初始标记。2.并发标记。3.最终标记。4.筛选回收

GC日志的理解

阅读GC日志是处理Java虚拟机内存问题的基础技能,它只是一些人为确定的规则,没有太多的技术含量。每个收集器的日志都可以不一样,但各个收集器的日志都维持一定的共性。

33.125和100.667:GC发生的时间,含义为从Java虚拟机启动以来进过的秒数。

[GC和[Full GC:说明垃圾收集的停顿类型

[DefNew和[Tenured和[Perm:表示GC发生的区域。

3324K->152(3712K):GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)

3324K->152K(11904K):GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)

0.0025925 secs:该内存区域GC所占用的时间,单位是秒。

垃圾收集器参数总结

 

内存分配与回收策略

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

最普遍的内存分配规则

1.对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。-XX:+PrintGCDetails收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程推出的时候输出当前的内存各区域分配情况。

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

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区吉两个Survivor区之间发生大量的内存复制。

 

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

4.动态年龄判定

5.空间分配担保

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值