您的位置:首页 > 其它

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

2017-09-28 10:17 204 查看
1. 简介

垃圾收集(Garbage Collection,GC)

1.1 使用场景

A.排查各种内存溢出、 内存泄漏问题时,

B.垃圾收集成为系统达到更高并发量的瓶颈, 需要对这些“自动化” 技术实施必要的监控和调节。

1.2 回收的内存区域

A.程序计数器、 虚拟机栈、 本地方法栈随线程而生,方法结束或者线程结束, 内存就回收了;

B.Java堆和方法区内存的分配和回收都是动态的, 垃圾收集器所关注的是这部分内存。

2. 垃圾回收

2.1 哪些对象可以回收

垃圾收集器在对堆进行回收前, 要确定这些对象之中哪些还“存活” 着, 哪些已经“死去” (即不可能再被任何途径使用的对象)。

A.引用计数算法

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

虚拟机并不是通过引用计数算法来判断对象是否存活的。

B.可达性分析算法

主流的商用程序语言(Java、 C#, 甚至包括前面提到的古老的Lisp) 的主流实现中, 都是称通过(Reachability Analysis) 来判定对象是否存活的。

基本思路就是通过一系列的称为“GC Roots” 的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链(Reference Chain) , 当一个对象到GC Roots没有任何引用链相连(用图论的话来说, 就是从GC Roots到这个对象不可达) 时, 则证明此对象不可用。



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

1).虚拟机栈(栈帧中的本地变量表) 中引用的对象。

2).方法区中类静态属性引用的对象。

3).方法区中常量引用的对象。

4).本地方法栈中JNI(即一般说的Native方法) 引用的对象

2.2 引用类型

参考:Java中的四种引用类型.note

Java对引用的概念进行了扩充, 将引用分为强引用 、 软引用 、 弱引用 、 虚引用4种, 这4种引用强度依次逐渐减弱。

A.强引用

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

B.软引用

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

C.弱引用

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

D.虚引用

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

2.3 回收过程

真正回收一个对象, 至少要经历两次标记过程:

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

B. 判定为有必要执行finalize()方法的对象,会将这个对象放置在一个叫做F-Queue的队列之中, 并在稍后由一个低优先级的Finalizer线程去执行它。GC将对F-Queue中的对象进行第二次小规模的标记, 如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可, 譬如把自己(this关键字) 赋值给某个类变量或者对象的成员变量, 那在第二次标记时它将被移除出“即将回收” 的集合; 如果对象这时候还没有逃脱, 那就真的被回收了。

注意:任何一个对象的finalize方法都只会被系统自动调用一次, 如果对象面临下一次回收, 它 的finalize方法不会被再次执行。

2.4 方法区的回收

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

类需要同时满足下面3个条件才能算是“无用的类” :

A.该类所有的实例都已经被回收, 也就是Java堆中不存在该类的任何实例。

B.加载该类的ClassLoader已经被回收。

C.该类对应的Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法。

HotSpot虚拟机提供了-Xnoclassgc参数进行控制, 还可以使用-verbose:class以及-XX: +TraceClassLoading、 -XX: +TraceClassUnLoading查看类加载和卸载信息。

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

3. 垃圾收集算法

3.1 标记-清除算法

最基础的收集算法是“标记-清除” (Mark-Sweep) 算法,算法分为“标记” 和“清除” 两个阶段: 首先标记出所有需要回收的对象, 在标记完成后统一回收所有被标记的对象,

缺点:一个是效率问题, 标记和清除两个过程的效率都不高;

另一个是空间问题, 标记清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时, 无法找到足够的连续内存而提前触发另一次垃圾收集动作。



3.2 复制算法

将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。 这样使得每次都是对整个半区进行内存回收, 内存分配时也就不用考虑内存碎片等复杂情况, 只要移动堆顶指针, 按顺序分配内存即可, 实现简单, 运行高效。



HotSpot采用这种收集算法来回收新生代:

将内存分为一块较大的Eden和两块较小的Survivor, 每次使用Eden和其中和from Survivor。 当回收时, 将Eden和Survivor中还存活着的对象一次性地复制到另外一块to Survivor空间上, 最后清理掉Eden和刚才用过的Survivor空间。 HotSpot默认Eden:from Survivor:to Survivor的大小比例是8:1:1,to Survivor占用10%的内存区域;

没有办法保证每次回收都只有不多于10%的对象存活, 当to Survivor空间不够用时, 需要依赖其他内存(这里指老年代) 进行分配担保(Handle Promotion) 。

3.3 标记-整理算法

根据老年代的特点,标记过程仍然与“标记-清除” 算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存。



3.4 分代收集算法

根据对象存活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中, 每次垃圾收集时都发现有大批对象死去, 只有少量存活, 那就选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集。 而老年代中因为对象存活率高、 没有额外空间对它进行分配担保, 就必须使用“标记—清理” 或者“标记—整理” 算法来进行回收。

HotSpot采用这种收集算法来进行垃圾收集;

4.HotSpot的算法实现

4.1 枚举根节点

可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性) 与执行上下文(例如栈帧中的本地变量表) 中,检查这些的引用的过程中,为了确保一致性的快照中进行,导致GC进行时必须停顿所有Java执行线程。

在HotSpot的实现中, 使用一组称为OopMap的数据结构来达到这个目的, 在类加载完成的时候HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,GC在扫描时就可直接得知这些信息。

4.2 安全点

A.什么是安全点

如果为每一条指令都生成OopMap, 会需要大量的额外空间, GC的成本将会变得很高,HotSpot没有为每条指令都生成OopMap, 在JIT编译过程中, 在特定的位置记录下栈和寄存器中哪些位置是引用, 这些位置称为安全点(Safepoint), 程序执行时在到达安全点时才能暂停。

B.选定规则

安全点的选定基本上是以程序“是否具有让程序长时间执行的特征” 为标准进行选定的,“长时间执行” 的最明显特征就是指令序列复用, 例如方法调用、 循环跳转、 异常跳转等, 所以具有这些功能的指令才会产生Safepoint。

C.如何让所有线程停在安全点

1).抢先式中断(Preemptive Suspension)

抢先式中断不需要线程的执行代码主动去配合, 在GC发生时, 首先把所有线程全部中断, 如果发现有线程中断的地方不在安全点上, 就恢复线程, 让它“跑” 到安全点上。 现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

2).主动式中断(Voluntary Suspension)

主动式中断的思想是当GC需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志, 各个线程执行时主动去轮询这个标志, 发现中断标志为真时就自己中断挂起。 轮询标志的地方和安全点是重合的, 另外再加上创建对象需要分配内存的地方。

4.3 安全区域

A.什么是安全区域

安全区域是指在一段代码片段之中, 引用关系不会发生变化。 在这个区域中的任意地方开始GC都是安全的。

B.执行过程

在线程执行到Safe Region中的代码时, 首先标识自己已经进入了Safe Region, 那样, 当在这段时间里JVM要发起GC时, 就不用管标识自己为Safe Region状态的线程了。 在线程要离开Safe Region时, 它要检查系统是否已经完成了根节点枚举(或者是整个GC过程) , 如果完成了, 那线程就继续执行, 否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

5. 垃圾收集器

收集算法是内存回收的方法论, 垃圾收集器就是内存回收的具体实现。



7种作用于不同分代的收集器, 两个收集器之间存在连线, 就说明它们可以搭配使用。 虚拟机

所处的区域, 则表示它是属于新生代收集器还是老年代收集器。

注意:只有对具体应用最合适的收集器,没有最好的收集器。

5.1 Serial收集器

Serial收集器是一个单线程的收集器, 它的“单线程” 的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作, 更重要的是在它进行垃圾收集时, 必须暂停其他所有的工作线程,直到它收集结束。它是虚拟机运行在Client模式下的默认新生代收集器。

Serial/Serial Old收集器的运行过程



5.2 ParNew收集器

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

ParNew/Serial Old收集器的运行过程



A.默认线程数

它是许多运行在Server模式下的虚拟机中首选的新生代收集器, 其中一个重要的原因是, 除了Serial收集器外,目前只有它能与CMS收集器配合工作。 默认开启的收集线程数与CPU的数量相同, 在CPU非常多的环境下, 可使用-XX: ParallelGCThreads限制垃圾收集的线程数。

B.名词定义

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

2).并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(但不一定是并行的, 可能会交替执行) , 用户程序在继续运行, 而垃圾收集程序运行于另一个CPU上。

5.3 Parallel Scavenge收集器

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,它目标则是达到一个可控制的吞吐量(Throughput) 。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值, 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) , 虚拟机总共运行了100分钟, 其中垃圾收集花掉1分钟, 那吞吐量就是99%。主要适合在后台运算而不需要太多交互的任务。Parallel Scavenge收集器也经常称为“吞吐量优先” 收集器。

A.设置吞吐量

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量, 分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX: GCTimeRatio参数。

B.其它重要参数

参数-XX: +UseAdaptiveSizePolicy,这是一个开关参数, 这个参数打开之后, 就不需要手工指定新生代的大小(-Xmn) 、 Eden与Survivor区的比例(-XX: SurvivorRatio) 、 晋升老年代对象年龄(-XX: PretenureSizeThreshold) 等细节参数了, 虚拟机会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量, 这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

5.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本, 它也是一个单线程收集器, 使用“标记-整理” 算法。它的主要在于给Client模式下的虚拟机使用。在Server模式下, 它主要还有两大用途: 一是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用, 另一种是作为CMS收集器的后备预案, 在并发收集发生Concurrent Mode Failure时使用。

5.5 Parallel Old收集器

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

Parallel Scavenge/Parallel Old收集器运行过程



5.6 CMS收集器

CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。适用于互联网站或者B/S系统的服务端上, 这类重视服务的响应速度, 希望系统停顿时间最短,以给用户带来较好的体验的应用。

A.运行过程

CMS收集器是基于“标记—清除” 算法实现的,整个过程分为4个步骤, 包括:

1).初始标记(CMS initial mark)

2).并发标记(CMS concurrent mark)

3).重新标记(CMS remark)

4).并发清除(CMS concurrent sweep)

初始标记、 重新标记这两个步骤需要“Stop The World” 。 初始标记只是标记一下GC Roots能直接关联到的对象, 速度很快, 并发标记就是进行GC RootsTracing的过程, 重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间一般会比初始标记阶段稍长一些, 但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作, 所以, 从总体上来说, CMS收集器的内存回收过程是与用户线程一起并发执行的。



B.特点

优点: 并发收集、 低停顿,也称为并发低停顿收集器(Concurrent Low Pause Collector) 。

缺点:

1).CMS收集器对CPU资源非常敏感;

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

3).收集结束时会有大量空间碎片产生。

5.7 G1收集器

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

A.特点

1).并行与并发

G1能充分利用多CPU、 多核环境下的硬件优势, 使用多个CPU(CPU或者CPU核心) 来缩短StopThe-World停顿的时间, 部分其他收集器原本需要停顿Java线程执行的GC动作, G1收集器仍然可以通过并发的方式让Java程序继续执行。

2).分代收集

分代概念在G1中依然得以保留。 G1可以不需要其他收集器配合就能独立管理整个GC堆, 它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、 熬过多次GC的旧对象以获取更好的收集效果。

3).可预测的停顿

降低停顿时间是G1和CMS共同的关注点, 但G1除了追求低停顿外, 还能建立可预测的停顿时间模型, 能让使用者明确指定在一个长度为M毫秒的时间片段内, 消耗在垃圾收集上的时间不得超过N毫秒, 这几乎已经是实时Java(RTSJ) 的垃圾收集器的特征了。

B.运行原理

它将整个Java堆划分为多个大小相等的独立区域(Region) , 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续) 的集

合。G1收集器之所以能建立可预测的停顿时间模型,是因为G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值) , 在后台维护一个优先列表, 每次根据允许的收集时间, 优先回收价值最大的Region(这也就是Garbage-First名称的来

由) 。 这种使用Region划分内存空间以及有优先级的区域回收方式, 保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

C.运行过程

G1收集器的运作大致可划分为以下几个步骤:

1).初始标记(Initial Marking)

2).并发标记(Concurrent Marking)

3).最终标记(Final Marking)

4).筛选回收(Live Data Counting and Evacuation)

初始标记和并发标记基本和CMS一样,最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这阶段需要停顿线程, 但是可并行执行。 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序, 根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行。



5.8 理解GC日志

5.9 垃圾收集器参数





6. 内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题: 给对象分配内存以及回收分配给对象的内存。

对象的内存分配, 往大方向讲, 就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配) , 对象主要分配在新生代的Eden区上, 如果启动了本地线程分配缓冲, 将按线程优先在TLAB上分配。

6.1 对象优先在Eden分配

新生代GC(Minor GC) : 指发生在新生代的垃圾收集动作, 因为Java对象大多都具备朝生夕灭的特性, 所以Minor GC非常频繁, 一般回收速度也比较快。

老年代GC(Major GC/Full GC) : 指发生在老年代的GC, 出现了Major GC, 经常会伴随至少一次的MinorGC(但非绝对的, 在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程) 。 Major GC的速度一般会比Minor GC慢10倍以上。

6.2 大对象直接进入老年代

所谓的大对象是指, 需要大量连续内存空间的Java对象。最典型的大对象就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置” 它们。

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

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

虚拟机给每个对象定义了一个对象年龄(Age) 计数器。 对象在Eden创建并经过第一次Minor GC后仍然存活, 并且能被Survivor容纳的话, 将被移动到Survivor空间中, 并且对象年龄设为1。 对象在Survivor区中每“熬过” 一次Minor GC, 年龄就增加1岁, 当它的年龄增加到一定程度(默认15岁),就会被晋升到老年代中。 对象晋升老年代的年龄阈值, 通过参数-XX:MaxTenuringThreshold设置。

6.4 动态对象年龄判定

为了能更好地适应不同程序的内存状况, 虚拟机并不是永远地要求对象的年龄必须达到了

MaxTenuringThreshold才能晋升老年代, 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 无须等到MaxTenuringThreshold中要求的年龄。

6.5 空间分配担保

JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC, 否则将进行Full GC。

6.6 示例

https://github.com/pingszi/JavaDemo.git(jvm->gc)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: