一文洞悉JVM内存管理机制

原文转载自 「掘金Android」 ( https://juejin.im/post/5e7d62f1e51d4546df7375dd ) By 许朋友爱玩🔥

预计阅读时间 0 分钟(共 0 个字, 0 张图片, 0 个链接)

前言

本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:

我的GIthub博客

学习导图:

学习导图

一.为什么要学习内存管理?

JavaC++之间有一堵由内存动态分配垃圾回收机制所围成的高墙,墙外面的人想进去,墙里面的人出不来

对于Java程序员来说,JVM给我们提供了自动内存管理机制,不需要既当“皇帝”,又当“人民”,不需要人为地给每一个new操作写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题。然而一旦出现内存泄漏和溢出方面的问题,如果不清楚JVM内存的内存管理机制,那么将很难定位与解决问题。而且,JVM的内存管理机制在面试中也是非常重要的考点之一。

综上,想要更加深入了解JVM的奥秘,探究JVM内存管理机制是必不可少的!!!

二.核心知识点归纳

2.1 JVM运行时数据区域

JVM 执行 Java 程序的过程:Java 源代码文件 (.java) 会被 Java 编译器编译为字节码文件(.class),然后由 JVM 中的类加载器加载各个类的字节码文件,加载完毕之后,交由 JVM 执行引擎执行

执行Java程序的过程

在上述过程中,JVM会用一段空间来存储执行程序期间需要用到的数据和相关信息,这段空间就是运行时数据区,也就是常说的JVM内存

JVM会将它所管理的内存划分为若干个不同的数据区域,划分结果如图:

JVM运行时数据区

可见,运行时数据区被分为线程私有数据区线程共享数据区两大类:

下面将为您详细介绍各个数据区的内容

2.1.1 程序计数器

  • 如果线程正在执行的是一个 Java 方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址
  • 如果线程正在执行的是一个 Native 方法,那么计数器的值则为

字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

2.1.2 Java 虚拟机栈

  • 每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息

  • 每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

局部变量表存放了编译期可知的各种基本数据类型、对象引用类型和 returnAddress 类型,它所需的内存空间在编译期间完成分配

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常
  • 如果虚拟机栈可动态扩展且扩展时无法申请到足够的内存,将抛出 OutOfMemoryError 异常

2.1.3 本地方法栈

想要了解Native方法的读者,可以看下这篇文章:Java中native方法

2.1.4 Java堆

Java 堆中,可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),但无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存

2.1.5 方法区

方法区装了啥

永久代/元空间 和方法区的区别:

  • 永久代/元空间 可看作是方法区的实现

2.1.6 运行时常量池

Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

Q1:字面量是什么

可以理解为字面意思的常量。

int a; //变量
const int b = 10; //b为常量,10为字面量
string str = “hello world!”; // str 为变量,hello world!为字面量
复制代码

由例子可知,字面量就是如此容易理解

Q2:符号引用是什么

可以是任意类型的字面量。只要能无歧义的定位到目标。在编译期间由于暂时不知道类的直接引用,因此先使用符号引用代替。最终还是会转换为直接引用访问目标

比如:java/lang/StringBuilder

Q3:运行时常量池是什么

2.1.7 直接内存

JDK1.4中新加入了NIO类,引入了基于通道与缓冲区的IO方式,可以使用Native函数库直接分配直接内存(堆外内存),然后通过DirectByteBuffer作为这块内存的引用进行操作

2.2 HotSpot 虚拟机内存对象探秘

在熟悉虚拟机内存划分及其具体内容之后,为详细了解虚拟机内存中数据的其他细节,以常用的虚拟机 HotSpot 和常用的内存区域 Java 堆为例,探讨 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程

2.2.1 对象的创建

遇到一个 new 指令后创建过程分三步

1.类加载检查

检查 new 指令的参数是否能在常量池中定位到一个类的符号引用且该符号引用代表的类是否已被加载、解析和初始化,若没有则需先执行相应的类加载,反之下一步

2.分配内存

  • Java 堆中的内存是否规整决定如何给新生对象分配可用空间
  • 由堆所采用的垃圾收集器是否带有空间压缩整理的能力决定Java 堆中的内存是否规整
  • 过程:将用过和空闲的内存放在两边,中间以一个指针作为分界指示器。当分配内存时,就把指针向空闲一边挪动与对象大小相等的距离即可
  • 应用:Serial、ParNew 等带 压缩过程的收集器
  • 过程:维护一个记录可用内存块的列表。当分配内存时,就从列表中找到一块足够大的空间划分给对象实例并更新记录
  • 应用:基于 Mark-Sweep 算法的 CMS 收集器

分配内存

保证内存分配是线程安全的解决方案:

  • 对内存分配的动作进行同步处理
  • 每个线程在 Java 堆中预先分配一块内存(本地线程分配缓冲 TLAB),在本线程的 TLAB 上进行分配,当 TLAB 用完需要分配新的 TLAB 时再同步锁定

3.设置对象头

将对象的所属类、找到类的元数据信息的方式、对象的哈希码、对象的 GC 分代年龄等信息存放在对象的对象头中

2.2.2 对象的内存分布

分为三块区域

对象的内存分布

  • Mark Word:用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等
  • 类型指针:用于确定这个对象的所属类

2.2.3 对象的访问定位

两种主流的访问方式

2.3 实战:OutOfMemoryError 异常

这部分的内容可以看下这篇文章:JVM内存溢出详解(栈溢出,堆溢出,持久代溢出、无法创建本地线程)

三.课堂小测试

恭喜你!已经看完了前面的文章,相信你对JVM内存管理机制已经有一定深度的了解,下面,进行一下课堂小测试,验证一下自己的学习成果吧!

Q1:JVM中,为什么要把堆与栈分离?栈不是也可以存储数据吗?

Q2:为啥说堆和JVM栈是程序运行的关键


如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

more_vert