java垃圾回收

2018-09-27  本文已影响0人  Crazy贵子

在java开发中,每个运行程序都会产生对象,JVM分配这些对象都需要一定的内存空间。由于java拥有垃圾收集器(GC),让我们不必专门去写内存回收代码。java的垃圾回收指的是回收内存,针对的java对象,所以涉及到JVM内存结构。此外,这些也是值得注意的问题:

首先看下JVM的内存结构,主要分为堆、栈、PC和方法区(在java 8后方法区不复存在,取而代之的是元数据区mataSpace,存放在本地内存,所以与之相关的Klass也被移除了,以下分析基于HotSpot虚拟机)。因为对象主要存在堆和方法区中(利用逃逸技术可以将一些对象标量分解后在栈上分配),GC关注的也是这些区域。

JVM内存结构

这里是一张更详细的图片

堆又分为年轻代和老年代,而年轻代中分为三部分Eden:FromSpace:ToSpace=8:1:1,FromSpace和ToSpace是等价的,两者身份是可以互换的。老年代空间比年轻代的要大。在创建对象时首先被分配到新生代,一般来说,大部分对象都是创建完后不久就不再使用,很快会被清理掉,如果经历了GC还在使用的就移到更持久的区域,年轻代内存划分成这样和回收的策略有关。

JVM为了区分哪些对象是需要回收的有如下方法:

在hotSpot中采用的是可达性分析算法。引用计数法是指当对象创建时,如果有新的引用指向它则加一,否则减一,当持有的引用数量为0时则标记为清理对象。这种方法在对象相互引用时无法识别出是否为待清理对象,比如:

public class Person {
    
    public Person friend;
    
    // ……
}

Person 阿珍 = new Person(); // 1
Person 阿强 = new Person(); // 2
阿珍.person = 阿强; // 3
阿强.person = 阿珍; // 4
阿珍 = null; // 5
阿强 = null; // 6

按照常理,阿珍和阿强这两个对象的引用已经没有了,应该是要被清理的,但引用计数法则判定不出。1和4操作后阿珍对象引用数为2,同理阿强也为2,5和6操作都只释放了1个引用,此时已经不能通过引用来访问到阿珍和阿强两个对象了,但由于两者相互引用导致引用数不为0,GC也就不会回收这两个对象,造成内存泄漏。

可达性分析采用的是图论的方法,对象之间的引用可记录为一张图,从根节点(GC Roots)出发,遍历这些对象,如果有不连通的对象,则标记为可回收对象。在JVM中,这些引用可以作为GC Roots:

刚才的例子如果采用可达性分析则如下图所示:

内存分配1 内存分配2 GC

将对象引用设为null后:

GC

寻找到可回收的对象后,GC就可以进行回收工作了,以下是回收算法:

标记-清除

将标记为可回收的对象直接清除 ,但这样会造成内存碎片产生,像老鼠打洞一样,这里空一点那里空一点,不利于内存再分配。

标记-整理

相当于在标记-清除的基础上添加了整理,把存活下来的对象往一个方向堆,类似于消消乐,消失的那部分会有其他对象从上面掉下来填充(这里的方向就是向下)。

复制

复制算法将内存划为大小相同的两部分,先用A内存,对A内存进行清除时遍历存活对象,将存活的对象全部复制到B,然后一口气将A内存清空,下一次对B内存清理时亦是如此。

因为大部分对象是朝生夕死,所以JVM的垃圾收集器根据其存活时间长短将堆划分为年轻代和老年代。可以预见,年轻代对象回收发生频率很高,所以采取复制算法比较好,一次性复制存活对象和清除回收对象,简单粗暴。而老年代是存活的对象比较多,需要回收的对象少,采用复制算法开销会比较大,所以采用标记-整理算法。不同区域采用不同的算法,这就是java的分代回收机制。

正是如此,年轻代才会将内存划分为8:1:1的布局。当Eden快满的时候,进行一次Minor GC,将存活对象复制到FromSpace,下次Minor GC的时候将FromSpaceEden的存活对象复制到ToSpace。这两个Survivor总有一个是空的,如果在复制的过程中发现有些对象达到了晋升老年代的条件(当这两个存活去切换了一定次数之后,默认是15次,可以使用-XX:MaxTenuringThreshold控制)则将其移动到老年代。为了加快Eden的对象回收和内存分配,HotSpot JVM采用了bump-the-point和TLAB(Thread-Local Allocation Buffers)两种技术。由于Eden是连续的, bump-the-point跟踪最后创建的一个对象,查看其后面是否有充足的内存,TLAB针对的是多线程,它将Eden划分为若干段,每个线程使用独立的一段。这两种技术结合起来可以快速地分配内存。如果对象比较大(比如大数组),可能会直接分配到老年代。当老年代快满的时候会触发一次Major GC。

这些都是对象回收的策略,根据这些策略,GC使用如下几种收集器对其进行管理。

HotSpot JVM1.6的垃圾收集器

相连收集器的可以配合使用。

根据并行和串行以及年轻代和老年代的划分,看下各个收集器的特点:

收集器 串行、并行 or 并发 新生代/老年代 算法 目标 使用场景
Serial 串行 新生代 复制 响应速度优先 单CPU环境下的Client模式
ParNew 并行 新生代 复制 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制 吞吐量优先 在后台运算而不需要太多交互的任务
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的java应用
G1 并发 both 标记-整理 + 复制 响应度优先 面向服务端应用,将来替换CMS

Serial收集器

单线程运行,进行GC的时候其他线程暂停,简单高效,是Hospot运行在Client模式下默认使用的新生代垃圾收集器。

ParNew

是Serial收集器的多线程版,是在Server模式下新生代的首选收集器,目前除了Serial收集器仅有它能和CMS(Concurrent Mark Sweep)配合工作。

Parallel Scavenge

多线程,关注GC时尽可能缩短用户线程的停顿时间,提高吞吐量(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + GC时间)),适用于不需要太多交互的后台运算。

Serial Old

单线程,使用标记-整理算法,在工作是其他线程需要停止。

Parallel Old

多线程,关注吞吐量,在注重吞吐量和CPU资源敏感的场合可以与Parallel Scavenge配合使用。

CMS

多线程并发运行,使用标记-清除算法,致力于获取最短停顿时间(缩短垃圾回收时间)。

CMS执行过程为:初始化标记->并发标记->预清理->可控清理->重新标记->并发清除->并发重设状态等待下次CMS的触发(先2次标记,1次预处理,1次重新标记,1次清除)

CMS运行需要额外的CPU和内存资源,所以在CPU内存紧张的情况下会采用Serial Old收集。同时因为采用的标记-清除算法,导致空间碎片产生。在CMS并发处理阶段由于用户线程还在运行,产生的新垃圾在本次清理中无法被清理,称为浮动垃圾,只能等待下CMS回收。

G1

多线程并行,采用标记-整理算法,运行在Server模式下

连续内存空间 Region

G1将整个堆内存划分为大小相同的独立区域(region),新生代和老年代在物理上不再分隔。当对象分配到一个region后它可以与整个堆的任意对象发生引用关系,进行扫描时需扫描整个堆,消耗很大。为了避免全堆扫描的发生,G1中每个Region会与一个Remembered Set关联,当对对象数据进行写操作时会产生一个Write Barrier暂时中断写操作,检查对象引用的对象是否处于不同的Region中,是则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中,扫描从GC Roots出发也就不会扫描全堆和有所遗漏了。

G1可以建立可预测的时间模型,它跟踪各个Region中垃圾的价值大小(回收获得的空间与所付出的时间),在后台维护一个优先列表,优先回收价值最大的Region。

G1的运行过程大致如下:

参考自:

深入理解JVM(3)——7种垃圾收集器
Java系列笔记(3) - Java 内存区域和GC机制
Java Hotspot G1 GC的一些关键技术

上一篇下一篇

猜你喜欢

热点阅读