前言

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

我的GIthub博客

学习导图:

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

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

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

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

二.核心知识点归纳

2.1 JVM运行时数据区域

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

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

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

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

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

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

2.1.1 程序计数器

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

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

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

2.1.2 Java 虚拟机栈

想更加详细了解 JVM 栈的读者,可以看下笔者写的这篇文章:运行时栈帧结构

  • 定义: 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)
  • JVM 所管理的内存中最大的一块
  • 可处于物理上不连续的内存空间中,只要逻辑上是连续的即可
  • Java 虚拟机规范中,如果在堆中没有内存完成实例分配,且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常

2.1.5 方法区

注意:方法区必须和虚拟机栈区分开,方法区不存方法,虚拟机栈存 Java 方法

  • 定义:与 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 指令的参数是否能在常量池中定位到一个类的符号引用且该符号引用代表的类是否已被加载、解析和初始化,若没有则需先执行相应的类加载,反之下一步

想详细了解类加载的知识的话,可以看下笔者的一篇文章:一夜搞懂 | JVM 类加载机制

2.分配内存

  • Java 堆中的内存是否规整决定如何给新生对象分配可用空间
  • 由堆所采用的垃圾收集器是否带有空间压缩整理的能力决定Java 堆中的内存是否规整

PS:想详细了解GC或者内存分配的话,可以看下笔者的这篇文章:一夜搞懂 | JVM GC&内存分配

  • 若规整,采用 “指针碰撞” 分配方式:
  • 过程:将用过和空闲的内存放在两边,中间以一个指针作为分界指示器。当分配内存时,就把指针向空闲一边挪动与对象大小相等的距离即可
  • 应用:SerialParNew 等带 压缩过程的收集器
  • 若非规整,采用 “空闲列表” 分配方式:
  • 过程:维护一个记录可用内存块的列表。当分配内存时,就从列表中找到一块足够大的空间划分给对象实例并更新记录
  • 应用:基于 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栈是程序运行的关键

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

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

本文参考链接:

一文洞悉JVM内存管理机制的更多相关文章

  1. JVM内存管理机制和垃圾回收机制

    JVM内存管理机制和垃圾回收机制 JVM结构 图片描述: java源码编译成class文件 class文件通过类加载器加载到内存 其中方法区存放的是运行时的常量.静态变量.类信息等,被所有线程共享 堆 ...

  2. 【JVM】5、JVM内存管理机制

    转自:http://blog.csdn.net/lengyuhong/article/details/5953544 近期看了看Java内存泄露的一些案例,跟原来的几个哥们讨论了一下,深入研究发现JV ...

  3. JVM内存管理机制

    Java与C++之间有一堆由内存动态分配与垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来. —— <深入理解Java虚拟机:JVM高级特性与最佳实践> Java虚拟机在 ...

  4. JVM 内存管理机制

    1. 内存分配图:  两栈一区一堆一计数 方法区里面 包含了运行时常量 2. 对象创建过程: new A() 首先加载A的字节码. 分配内存,内存分配方式分两种,如果采用带压缩的垃圾回收策略,则采用“ ...

  5. Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收

    很多Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收肯定要涉及到JVM内存管理机制,Java语言的执行效率一直被C.C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确 ...

  6. Spark内存管理机制

    Spark内存管理机制 Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色.理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行 ...

  7. JVM内存管理及垃圾回收【转】

    很多Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收肯定要涉及到JVM内存管理机制,Java语言的执行效率一直被C.C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确 ...

  8. 你应该这样理解JVM内存管理

    在进行Java程序设计时,一般不涉及内存的分配和内存回收的相关代码,此处引用一句话: Java和C++之间存在一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,墙里面的人想出来 ,个人从这 ...

  9. 你必须了解的java内存管理机制(一)-运行时数据区

    前言 本打算花一篇文章来聊聊JVM内存管理机制,结果发现越扯越多,于是分了四遍文章(文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8),本文为其中第一篇.from 你必须了解的java内存 ...

随机推荐

  1. Java equals和==的理解

    一.简介 ==: == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象.比较的是真正意义上的指针操作. 1.比较的是操作符两端的操作数是否是 ...

  2. Jmeter之Beanshell---使用Java处理JSON块

    原文出处:https://www.cnblogs.com/xpp142857/p/7374281.html [环境] ①Jmeter版本:3.1,JDK:1.7 ②前置条件:将json.jar包置于. ...

  3. kNN算法 Demo

    项目链接: https://github.com/WES6/kNN

  4. ERROR: Error in Log_event::read_log_event(): 'Found invalid event in binary log', data_len: 31, event_type: 35报错处理

    centos7系统MySQL5.7在用mysqlbinlog命令查询binlog日志时刚开始查询即自动终止查询,查了一下该日志有300M,于是仔细看发现有报错,见下图: 在网上查找经验贴http:// ...

  5. Python的Flask框架开发RESTful API

    web框架选择 Django,流行但是笨重,还麻烦,人生苦短,肯定不选 web.py,轻量,但据说作者仙逝无人维护,好吧,先pass tornado,据说倡导自己造轮子,虽然是facebook开源的吧 ...

  6. sql问题处理

    批量杀死MySQL连接 select concat('KILL ',id,';') from information_schema.processlist where Info like 'selec ...

  7. oppo互联网招聘-各类软件测试

    一.服务端测试专家 关键词:安全测试.白盒测试.性能测试.自动化.持续集成.服务端 岗位职责: 主导多个高日活产品的测试方案: 试点和推广自动化和持续集成: 改善测试相关流程和规范. 职位要求: 计算 ...

  8. MFC中文件对话框类CFileDialog详解及文件过滤器说明

    当前位置 : 首页 » 文章分类 :  开发  »  MFC中文件对话框类CFileDialog详解及文件过滤器说明 上一篇 利用OpenCV从摄像头获得图像的坐标原点是在左下角 下一篇 Word中为 ...

  9. SpringMVC 使用注解完成登录拦截

    目录 为了实现用户登录拦截你是否写过如下代码呢? 1. 基于Filter 2. 基于Struts 3. 基于SpringMVC 如何使用自定义注解完成自定义拦截呢? 登录注解 SpringMVC 拦截 ...

  10. 成长日记(2) Java面向对象

    本篇主要是记录自己在学习路上的笔记,如果有哪里记错了请大家直接指出 面向对象的概念 *人为抽象的一种编程模型 *面向过程 代码集中 难以维护 *类:对事物 算法 逻辑 概念等的抽象 理解成 模板 图纸 ...