一文洞悉JVM内存管理机制

前言

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

个人GIthub博客java

学习导图:

学习导图

一.为何要学习内存管理?

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

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

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

二.核心知识点概括

2.1 JVM运行时数据区域

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

执行Java程序的过程

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

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

JVM运行时数据区

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

  • 线程私有数据区包含:程序计数器、虚拟机栈、本地方法栈
  • 线程共享数据区包含:Java堆、方法区(内部包含运行时常量池

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

2.1.1 程序计数器

  • 定义:当前线程所执行的字节码的行号指示器
  • 若是线程正在执行的是一个 Java 方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址
  • 若是线程正在执行的是一个 Native 方法,那么计数器的值则为

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

  • 为何必须是私有:为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,所以它是线程私有的内存
  • 在《 Java 虚拟机规范》中,是惟一一个没有规定任何 OutOfMemoryError 状况的区域

2.1.2 Java 虚拟机栈

  • 定义: Java 方法执行的内存模型
  • 每一个方法在执行的同时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法出口等信息

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

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

  • 线程私有的内存,与线程生命周期相同
  • 通常把 Java 内存区分为堆内存(Heap)和栈内存(Stack),其中『栈』指的是虚拟机栈,『堆』指的是 Java
  • Java 虚拟机规范中,对这个区域规定了两种异常情况:
  • 若是线程请求的栈深度大于虚拟机所容许的深度,将抛出 StackOverflowError 异常
  • 若是虚拟机栈可动态扩展且扩展时没法申请到足够的内存,将抛出 OutOfMemoryError 异常

2.1.3 本地方法栈

  • 定义:虚拟机使用到的 Native 方法服务

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

  • 在虚拟机规范中,对这个区域无强制规定,由具体的虚拟机自由实现。与虚拟机栈同样,本地方法栈区域也会抛出 StackOverflowErrorOutOfMemoryError 异常

2.1.4 Java堆

  • 定义:被全部线程共享的一块内存区域,在虚拟机启动时建立
  • 做用:用于存放几乎全部的对象实例和数组

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

  • 是垃圾收集器管理的主要区域,也被称作 “GC 堆”(可别叫作垃圾堆orz)
  • Java 虚拟机所管理的内存中最大的一块
  • 可处于物理上不连续的内存空间中,只要逻辑上是连续的便可
  • Java 虚拟机规范中,若是在堆中没有内存完成实例分配,且堆也没法再扩展时,将会抛出 OutOfMemoryError 异常

2.1.5 方法区

  • 定义:与 Java 堆同样,是各个线程共享的内存区域

  • 做用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

方法区装了啥

  • 人们更愿意把这个区域称为 “永久代”,它还有个别名叫作 Non-Heap(非堆)

    JDK7HotSpot 中,已经把本来放在永久代的字符串常量池静态变量移出;

    JDK8中,废弃永久代的概念,改用元空间

  • 对用元空间替换永久代的缘由感兴趣的话,能够看下这篇文章:一文读懂 - 元空间和永久代

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

  • 永久代/元空间 可看做是方法区的实现
  • Java 堆同样不须要连续的内存和能够选择固定大小或可扩展外,还可选择不实现 GC
  • Java 虚拟机规范中,当方法区没法知足内存分配需求时,将抛出 OutOfMemoryError 异常

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:运行时常量池是什么

  • 相对于 Class 文件常量池的一个重要特征是具有动态性,体如今并不是只有预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中
  • 方法区的一部分,会受到方法区内存的限制
  • Java 虚拟机规范中,当常量池没法再申请到内存时会抛出 OutOfMemoryError 异常

2.1.7 直接内存

  • 它并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,可是这部份内存也被频繁地调用
  • 做用:避免了在JAVA堆和Native堆中来回复制数据,所以在一些场景下能显著提升性能

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、偏向时间戳等
  • 类型指针:用于肯定这个对象的所属类
  • 实例数据:存储真正的有效信息,是程序代码中定义的各类类型的字段内容。存储顺序会受虚拟机分配策略参数和字段在 Java 源码中定义顺序这两个因素影响。
  • 对齐填充:占位符,帮助补全未对齐的对象实例数据部分(保证是 8 字节的倍数),非必需

2.2.3 对象的访问定位

两种主流的访问方式

  • 经过句柄访问对象

    Java 堆中划分出一块内存来做为句柄池,reference 存储的是对象的句柄地址,在句柄中包含了对象实例数据与类型数据各自的具体地址信息

    好处:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 自己不须要修改

    经过句柄访问对象

  • 经过直接指针访问对象

    Java 堆对象的布局中考虑如何放置访问类型数据的相关信息,reference 存储的直接就是对象地址

    好处:速度更快,节省了一次指针定位的时间开销

    经过直接指针访问对象

2.3 实战:OutOfMemoryError 异常

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

三.课堂小测试

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

Q1:JVM中,为何要把堆与栈分离?栈不是也能够存储数据吗?

  • 从软件设计的角度看,栈表明了处理逻辑,而堆表明了数据,分工明确,处理逻辑更为清晰体现了“分而治之”以及“隔离”的思想。

  • 堆与栈的分离,使得堆中的内容能够被多个栈共享(也能够理解为多个线程访问同一个对象)。这样共享的方式有不少收益:提供了一种有效的数据交互方式(如:共享内存);堆中的共享常量和缓存能够被全部栈访问,节省了空间。

  • 栈由于运行时的须要,好比保存系统运行的上下文,须要进行地址段的划分。因为栈只能向上增加,所以就会限制住栈存储内容的能力。而堆不一样,堆中的对象是能够根据须要动态增加的,所以栈和堆的拆分,使得动态增加成为可能,相应栈中只需记录堆中的一个地址便可。

  • 堆和栈的结合完美体现了面向对象的设计。当咱们将对象拆开,你会发现,对象的属性便是数据,存放在堆中;而对象的行为(方法)便是运行逻辑,放在栈中。所以编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。

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

  • 栈是运行时的单位(解决程序的运行问题,即程序如何执行,或者说如何处理数据),而堆是存储的单位(解决的是数据存储的问题,即数据怎么放、放在哪儿)
  • 堆存储的是对象。栈存储的是基本数据类型和堆中对象的引用;(参数传递的值传递和引用传递)

若是文章对您有一点帮助的话,但愿您能点一下赞,您的点赞,是我前进的动力

本文参考连接:

相关文章
相关标签/搜索