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

2018-10-06  本文已影响3人  沐小晨曦

概述

在前面介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,也就是随着线程而生,伴随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

对象是否存活?

垃圾收集器在对堆进行回收前,首先要判断对象是否不被使用了。判活有以下几种办法:

再谈引用

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用这四种。

垃圾收集算法

标记 - 清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它主要有两点不足:一个是效率问题,标记和清除两个过程的效率都不高;另外一个是空间问题,标记清除后会产生大量的内存碎片。

image

复制算法

为了解决效率问题,复制算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将还存活的对象复制到另外一块,然后将已使用的那块内存空间一次清理掉。实现简单,运行高效。但是这种算法的代价就是将内存缩小了原来的一般。

image

现在虚拟机都采用复制算法来回收新生代。新生代的对象98%的对象都是朝生夕死的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1 。也就是只有10%的内存会“浪费”。当然,如果Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

标记 - 整理算法

复制算法在对象存活率比较高的时候是非常低效的,更关键的是,如果不想浪费掉50%的内存空间,就要有额外的空间进行分配担保。所以老年队一般不会选用复制算法。

和标记清除算法的标记过程一致,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

image

HotSpot的算法实现

枚举根结点

可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须确保一致性。一致性的意思是指在整个分析过程中整个系统看起来像被冻结在某个时间点上,不可以出现分析过程中引用关系还在不断变化的情况,这点是导致GC进行时必须停顿掉所有的线程。

安全点

程序执行时并非在所有的地方都能停顿下来开始GC,只有在到达安全点时才能暂停。那么如何让GC发生时所有的线程都跑到安全点上在停顿下来呢?有两种方案可供选择:抢先式中断和主动式中断。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

主动式中断的思想是当GC需要中断线程时,不直接对线程操作,仅仅是设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志时就自己中断挂起。轮询标志的地方和安全点是重合的。

安全区

可以看作是被扩展的安全点。

垃圾收集器

如果说垃圾收集算法是对内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

image

如果两个收集器之间存在连线,就说明它们可以搭配使用。

Serial收集器

单线程收集器,它在进行垃圾收集时,必须暂停其他所有的工作线程。

ParNew收集器

其实是Serial收集器的多线程版本。

Parallel Scavenge收集器

是一个使用复制算法的新生代收集器,又是并行的多线程收集器。但是它的关注点和其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。例如虚拟机总运行了100分钟,其中垃圾回收花费了一分钟,那么吞吐量就是99%。

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

Serial Old收集器

是Serial收集器的老年队版本,它同样是一个单线程收集器,使用标记-整理算法。

Parallel Old收集器

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

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现,并发收集,低停顿。

G1收集器

G1是一款面向服务端应用的垃圾收集器。有以下特点:

内存分配和回收策略

对于内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲区,将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代。

对象优先在Eden区分配

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

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。一般来说,超过3M的对象会直接在老年代进行分配。

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

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收就必须能识别哪些对象应放在新生代还是老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就会增加一岁。当它的年龄增加到一定程度(默认是15),就将会被晋升到老年代中。

上一篇下一篇

猜你喜欢

热点阅读