JVM 内存布局及 GC 原理

摘要:
“java内存布局和GC原理”是java开发人员无法回避的话题,也是面试中常见的问题之一。如上图所示,尽管堆外内存不受JVM控制,但堆内存将保存对它的引用以供GC使用。在JVM内存布局的第一部分,我们知道线程具有独占区域:PCRegister、JVMStack和NativeMethodStack。它们的生命周期与线程的生命周期相同,因此不需要GC。线程共享的堆区域和MethodArea是GC的关键对象。

“java 的内存布局以及 GC 原理”是 java 开发人员绕不开的话题,也是面试中常见的高频问题之一。

java 发展历史上出现过很多垃圾回收器,各有各的适应场景,很多网上的旧文章已经跟不上最新的变化。本文详细介绍了 java 的内存布局以及各种垃圾回收器的原理(包括最新的 ZGC),希望阅读完后,大家对这方面的知识不再陌生,有所收获,同时也欢迎大家留言讨论。

一、JVM 运行时内存布局

 java 8 虚拟机规范的原始表达:(jvm)Run-Time Data Areas, 暂时翻译为“jvm 运行时内存布局”。

从概念上大致分为 6 个(逻辑)区域,参考下图。注:Method Area 中还有一个常量池区,图中未明确标出。

一文看懂JVM内存布局及GC原理

这 6 块区域按是否被线程共享,可以分为两大类:

一文看懂JVM内存布局及GC原理

一类是每个线程所独享的:

1)PC Register:也称为程序计数器, 记录每个线程当前执行的指令信。eg:当前执行到哪一条指令,下一条该取哪条指令。

2)JVM Stack:也称为虚拟机栈,记录每个栈帧(Frame)中的局部变量、方法返回地址等。注:这里出现了一个新名词“栈帧”,它的结构如下:

一文看懂JVM内存布局及GC原理

线程中每次有方法调用时,会创建Frame,方法调用结束时Frame 销毁。

3)Native Method Stack:本地 (原生) 方法栈,顾名思义就是调用操作系统原生本地方法时,所需要的内存区域。

上述 3 类区域,生命周期与 Thread 相同,即:线程创建时,相应的区域分配内存,线程销毁时,释放相应内存。

另一类是所有线程共享的:

1)Heap:即鼎鼎大名的堆内存区,也是 GC 垃圾回收的主站场,用于存放类的实例对象及 Arrays 实例等。

2)Method Area:方法区,主要存放类结构、类成员定义,static 静态成员等。

3)Runtime Constant Pool:运行时常量池,比如:字符串,int -128~127 范围的值等,它是 Method Area 中的一部分。

Heap、Method Area 都是在虚拟机启动时创建,虚拟机退出时释放。

注:Method Area 区,虚拟机规范只是说必须要有,但是具体怎么实现(比如: 是否需要垃圾回收? ),交给具体的 JVM 实现去决定,逻辑上讲,视为 Heap 区的一部分。所以,如果你看见类似下面的图,也不要觉得画错了。

一文看懂JVM内存布局及GC原理

上述 6 个区域,除了 PC Register 区不会抛出 StackOverflowError 或 OutOfMemoryError ,其它 5 个区域,当请求分配的内存不足时,均会抛出 OutOfMemoryError(即:OOM),其中 thread 独立的 JVM Stack 区及 Native Method Stack 区还会抛出 StackOverflowError。

最后,还有一类不受 JVM 虚拟机管控的内存区,这里也提一下,即:堆外内存。

一文看懂JVM内存布局及GC原理

可以通过 Unsafe 和 NIO 包下的 DirectByteBuffer 来操作堆外内存。如上图,虽然堆外内存不受 JVM 管控,但是堆内存中会持有对它的引用,以便进行 GC。

提一个问题:总体来看,JVM 把内存划分为“栈 (stack)”与“堆 (heap)”两大类,为何要这样设计?

个人理解,程序运行时,内存中的信息大致分为两类,一是跟程序执行逻辑相关的指令数据,这类数据通常不大,而且生命周期短;一是跟对象实例相关的数据,这类数据可能会很大,而且可以被多个线程长时间内反复共用,比如字符串常量、缓存对象这类。

将这两类特点不同的数据分开管理,体现了软件设计上“模块隔离”的思想。好比我们通常会把后端 service 与前端 website 解耦类似,也更便于内存管理。

二、GC 垃圾回收原理

2.1 如何判断对象是垃圾 ?

有两种经典的判断方法,借用网友的图(文中最后有给出链接):

一文看懂JVM内存布局及GC原理

引用计数法,思路很简单,但是如果出现循环引用,即:A 引用 B,B 又引用 A,这种情况下就不好办了,所以 JVM 中使用了另一种称为“可达性分析”的判断方法:

一文看懂JVM内存布局及GC原理

还是刚才的循环引用问题(也是某些公司面试官可能会问到的问题),如果 A 引用 B,B 又引用 A,这 2 个对象是否能被 GC 回收?

答案:关键不是在于 A、B 之间是否有引用,而是 A、B 是否可以一直向上追溯到 GC Roots。如果与 GC Roots 没有关联,则会被回收,否则将继续存活。

一文看懂JVM内存布局及GC原理

上图是一个用“可达性分析”标记垃圾对象的示例图,灰色的对象表示不可达对象,将等待回收。

2.2 哪些内存区域需要 GC ?

一文看懂JVM内存布局及GC原理

在第一部分 JVM 内存布局中,我们知道了 thread 独享的区域:PC Regiester、JVM Stack、Native Method Stack,其生命周期都与线程相同(即:与线程共生死),所以无需 GC。线程共享的 Heap 区、Method Area 则是 GC 关注的重点对象。

2.3 常用的 GC 算法

1)mark-sweep 标记清除法

一文看懂JVM内存布局及GC原理

如上图,黑色区域表示待清理的垃圾对象,标记出来后直接清空。该方法简单快速,但是缺点也很明显,会产生很多内存碎片。

2)mark-copy 标记复制法

一文看懂JVM内存布局及GC原理

思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用 50% 的内存。

3)mark-compact 标记 - 整理(也称标记 - 压缩)法

一文看懂JVM内存布局及GC原理

避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低 GC 的效率。

4)generation-collect 分代收集算法

上述三种算法,每种都有各自的优缺点,都不完美。在现代 JVM 中,往往是综合使用的,经过大量实际分析,发现内存中的对象,大致可以分为两类:有些生命周期很短,比如一些局部变量 / 临时对象,而另一些则会存活很久,典型的比如 websocket 长连接中的 connection 对象,如下图:

一文看懂JVM内存布局及GC原理

纵向 y 轴可以理解分配内存的字节数,横向 x 轴理解为随着时间流逝(伴随着 GC),可以发现大部分对象其实相当短命,很少有对象能在 GC 后活下来。因此诞生了分代的思想,以 Hotspot 为例(JDK 7):

一文看懂JVM内存布局及GC原理

将内存分成了三大块:年青代(Young Genaration),老年代(Old Generation), 永久代(Permanent Generation),其中 Young Genaration 更是又细为分 eden,S0,S1 三个区。

结合我们经常使用的一些 jvm 调优参数后,一些参数能影响的各区域内存大小值,示意图如下:

一文看懂JVM内存布局及GC原理

注:jdk8 开始,用 MetaSpace 区取代了 Perm 区(永久代),所以相应的 jvm 参数变成 -XX:MetaspaceSize 及 -XX:MaxMetaspaceSize。

以 Hotspot 为例,我们来分析下 GC 的主要过程:

刚开始时,对象分配在 eden 区,s0(即:from)及 s1(即:to)区,几乎是空着。

一文看懂JVM内存布局及GC原理

随着应用的运行,越来越多的对象被分配到 eden 区。

一文看懂JVM内存布局及GC原理

当 eden 区放不下时,就会发生 minor GC(也被称为 young GC),第 1 步当然是要先标识出不可达垃圾对象(即:下图中的黄色块),然后将可达对象,移动到 s0 区(即:4 个淡蓝色的方块挪到 s0 区),然后将黄色的垃圾块清理掉,这一轮过后,eden 区就成空的了。

注:这里其实已经综合运用了“【标记 - 清理 eden】 + 【标记 - 复制 eden->s0】”算法。

一文看懂JVM内存布局及GC原理

随着时间推移,eden 如果又满了,再次触发 minor GC,同样还是先做标记,这时 eden 和 s0 区可能都有垃圾对象了(下图中的黄色块),注意:这时 s1(即:to)区是空的,s0 区和 eden 区的存活对象,将直接搬到 s1 区。然后将 eden 和 s0 区的垃圾清理掉,这一轮 minor GC 后,eden 和 s0 区就变成了空的了。

一文看懂JVM内存布局及GC原理

继续,随着对象的不断分配,eden 空可能又满了,这时会重复刚才的 minor GC 过程,不过要注意的是,这时候 s0 是空的,所以 s0 与 s1 的角色其实会互换,即:存活的对象,会从 eden 和 s1 区,向 s0 区移动。然后再把 eden 和 s1 区中的垃圾清除,这一轮完成后,eden 与 s1 区变成空的,如下图。

一文看懂JVM内存布局及GC原理

对于那些比较“长寿”的对象一直在 s0 与 s1 中挪来挪去,一来很占地方,而且也会造成一定开销,降低 gc 效率,于是有了“代龄 (age)”及“晋升”。

对象在年青代的 3 个区 (edge,s0,s1) 之间,每次从 1 个区移到另 1 区,年龄 +1,在 young 区达到一定的年龄阈值后,将晋升到老年代。下图中是 8,即:挪动 8 次后,如果还活着,下次 minor GC 时,将移动到 Tenured 区。

一文看懂JVM内存布局及GC原理

下图是晋升的主要过程:对象先分配在年青代,经过多次 Young GC 后,如果对象还活着,晋升到老年代。

一文看懂JVM内存布局及GC原理

如果老年代,最终也放满了,就会发生 major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少 Full GC 的原因。

一文看懂JVM内存布局及GC原理

注:上面的过程主要来自 oracle 官网的资料,但是有一个细节官网没有提到,如果分配的新对象比较大,eden 区放不下,但是 old 区可以放下时,会直接分配到 old 区(即没有晋升这一过程,直接到老年代了)。

https://www.infoq.cn/article/3WyReTKqrHIvtw4frmr3

免责声明:文章转载自《JVM 内存布局及 GC 原理》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Django之无名分组,有名分组iOS 在视图控制器里面判断 应用程序的前台 后台切换 UIViewController下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

java 虚拟机(五) jvm参数及调优

JVM 性能监控工具 一、Jinfo 1、用于查看正在运行的Java应用程序的扩展参数 `` jinfo -flags 线程号 `` 2、查看java系统参数 jinfo sysprops 线程号 二、Jstat 用于查看堆栈信息 jstat -class 线程号 其中 Loaded:加载class的数量 Bytes:所占用空间大小 Unloa...

Javascript 基础夯实 —— 使用 webWorker 实现多线程(转)

原文链接:https://zhuanlan.zhihu.com/p/29219879 当我们开始学习 javascript 的时候,我们就知道 js 其实是单线程的,所以当我们在浏览器中运行某些耗时算法或者阻塞线程的代码时,浏览器就会出现卡顿的现象 然而 js 引擎却拥有多个线程,比如渲染界面线程、浏览器事件触发线程、http 请求线程、事件轮询处理线程等...

从深圳回武汉的面试感想以及一些面试题

  转载:https://www.cnblogs.com/itdragon/p/9026994.html 大家好,我是ITDragon龙,今天分享一些面试中常问的题目,和一些面试感受。我是四月二十五号离职,二十六号上午八点从深圳出发,下午三点到武汉。二十七号便开始参加面试。每天的节奏基本是:上午去公司面试,中午去小区房子装修监工,下午再去公司面试,中途还...

windows 编程钩子技术初尝(hook)

最近在研究hook这个东西,作为一个windows菜鸟,研究这个还真花了点时间,下面分享下今天按照别人代码写出的两个鼠标钩子实例。 第一个是针对线程的钩子 几点需要说明的地方:   (1) 如果对于同一事件(如鼠标消息)既安装了线程钩子又安装了系统钩子,那么系统会自动先调用线程钩子,然后调用系统钩子。   (2) 对同一事件消息可安装多个钩子处理过程,这些...

【VC++积累】之五、进程注入技术

注入:就是把我的代码,添加到已经远行的远程进程的方法; 在WinNT以后的系列操作系统中,每个进程都有自己的4GB私有进程地址空间,彼此互不相关。 如 :   进程A中的一个地址,比如:0x12345678,到了进程B中的相同地方,存的东西完全不一样,或者说不可预料。            所以说如果进程A想要看看或者修改进程B地址空间中的内容,就必须深入...

多线程详解,一篇文章彻底搞懂多线程中各个难点

出自https://mp.weixin.qq.com/s/xh-LfrelAgFCRkP5-s-GLw 1.什么是线程? linux内核中是没有线程这个概念的,而是轻量级进程的概念:LWP。一般我们所说的线程概念是C库当中的概念。 1.1线程是怎样描述的? 线程实际上也是一个task_struct,工作线程拷贝主线程的task_struct,然后共用主线程...