java垃圾回收

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

  • GC是怎么判断哪些对象需要回收
  • 采取哪些措施回收,什么时候回收
  • 落实到GC中是什么样子

首先看下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:

  • 栈中的对象引用
  • 方法区中的静态对象引用和常量对象引用
  • 本地方法栈JNI的对象引用

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

内存分配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的垃圾收集器

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

  • 新生代收集器:Serial、ParNew、Parallel Scavenge

  • 老年代收集器:CMS、Serial Old、Parallel Old

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

收集器 串行、并行 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次清除)

  • 初始化标记:仅标记GC Roots能关联到的对象
  • 并发标记:进行GC Roots Tracing,整个过程耗时最长
  • 重新标记:为了修正用户在运行时导致标记变动那一部分对象的标记记录

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的运行过程大致如下:

  • 初始标记:仅标记GC Roots能直接到达的对象
  • 并发标记:开始对heap中的对象标记,标记线程与应用程序线程并行执行,并收集各个Region的存活对象。
  • 最终标记:并发标记会将对象的修改记录到Remmembered Set Logs中,在本阶段将RSL整合到RS中。
  • 筛选回收:对Region中的回收价值进行排序,此阶段时间用户可控制。

参考自:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容