JVM技术专题深入分析内存布局及GC原理分析「上卷」

Posted 浩宇の天尚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM技术专题深入分析内存布局及GC原理分析「上卷」相关的知识,希望对你有一定的参考价值。

前提概要

JVM虚拟机的整体的结构分布图,我个人觉得比较难掌握和理解的问题主要集中在“GC回收(内存管理)” 和 “内存布局”,这两部分属于真正的核心部分,至于执行引擎,可以理解为X86的寄存器执行方式或者stack栈式调用方式,本文暂时不考虑和介绍。


“java 的内存布局以及 GC 原理” 是 java 开发人员绕不开的话题,也是面试中常见的高频问题之一。希望阅读完后,大家对这方面的知识不再陌生,有所收获,同时也欢迎大家留言讨论。

内存布局

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

Java8虚拟机规范


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

  • 1.类加载器(Class Loader):加载字节码文件到内存
  • 2.运行时数据区(Runtime Data Area):JVM核心内存空间结构模型
  • 3.执行引擎(Execution Engine):对 JVM 指令进行解析,翻译成机器码,解析完成后提交到操作系统中
  • 4.本地库接口(Native Interface):供 Java 调用的融合了不同开发语言的原生库。
  • 5.本地方法库(Native Libraies):Java 本地方法的具体实现。

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

 此处声明一下,因为考虑到寄存器有不同的种类,pc寄存器主要是属于指令寄存器,而操作数栈中的寄存器属于数据寄存器

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

  • 1)PC Register:也称为程序计数器, 记录每个线程当前执行的指令信。eg:当前执行到哪一条指令,下一条该取哪条指令。 (会存在中断向量表)
  • 2)JVM Stack:也称为虚拟机栈,记录每个栈帧(Frame)中的局部变量、方法返回地址等。注:这里出现了一个新名词“栈帧”。它的结构如下:

 线程中每次有方法调用时,会创建 Frame,方法调用结束时 Frame 销毁。
  • 3)Native Method Stack:本地(原生)方法栈,顾名思义就是调用操作系统原生本地方法时,所需要的内存区域。

在 Java 程序中声明 native 修饰的方法,只有方法定义,没有方法实现,将该 Java 文件编译成字节码文件。

1. 用 javah 编译字节码文件,生成一个 .h 文件。
2. 写一个 .cpp 文件实现 .h 文件中的方法。
3. 将 .cpp 文件编译成动态链接库文件 .dll 。
4. 使用 System.loadLibrary() 加载动态连接库文件。

这样就可以实现本地方法的调用,用 Java 调用而非 Java 编写的接口,基本原理是利用反射机制,在运行时找到 .dll 文件并且解析,根据动态链接库中的文件名称创建出对象和方法,然后就可以利用对象调用方法了。

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

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

  • 1)Heap Area:堆内存区,GC垃圾回收的主站场,用于存放类的实例对象及 Arrays 实例等。
  • 2)Method Area:方法区,存放类或者接口结构、类成员定义,static 静态成员等。
  • 3)Runtime Constant Pool:运行时常量池,比如:字符串,IntegerCache(etc) -128~127 范围的值等,一些采用final修饰的字段和对象属性吗,它是 Method Area 中的一部分。

特别注意:

方法区存储虚拟机加载的类信息、常量、静态变量及即时编译器编译后的代码等数据。

方法区是一种规范,它的其中一种实现是永久代。

  • JDK 7 以前的版本字符串常量池是放在永久代中的;
  • JDK 7 将字符串常量池移动到了堆中;
  • JDK 8 直接删除了永久代,改用元空间替代永久代。(字符串常量池仍存放在堆);

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

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

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


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


可以通过 UnsafeNIO 包下的 DirectByteBuffer 来操作堆外内存。如上图,虽然堆外内存不受 JVM 管控(不在对象heap区域分配内存),但是堆内存中会持有对它的引用,以便进行 GC

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


个人理解,程序运行时,内存中的信息大致分为两类:

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

举个案例:就如同方法区和堆和栈之间的联系图:


GC垃圾回收原理

如何判断对象是垃圾 ?


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

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


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

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


  • 线程独享的区域:PC Register、JVM Stack、Native Method Stack,其生命周期都与线程相同(即:与线程共生死),所以无需 GC。
  • 线程共享的区域:Heap Area、Method Area 则是 GC 关注的重点对象。

常用的GC算法

mark-sweep 标记清除法

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

mark-copy 标记复制法

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

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

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

generation-collect 分代收集算法

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

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

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

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

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

以上是关于JVM技术专题深入分析内存布局及GC原理分析「上卷」的主要内容,如果未能解决你的问题,请参考以下文章

JVM技术专题深入分析内存布局及GC原理分析「中卷」

JVM技术专题「原理专题」深入剖析Java对象内存分配及跨代引用分析

JVM技术专题深入分析CG管理和原理查缺补漏「番外篇」

JVM技术专题「原理专题」全流程分析Java对象的创建过程及内存布局

JVM技术专题深入研究JVM内存逃逸原理分析「研究篇」

JVM技术专题「原理专题」深入分析Java中finalize方法的作用和底层原理