前言

本文已经收录到我的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. Python---12函数式编程------12.1高阶函数

    函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...

  2. Windows系统在Python2.7环境下安装numpy, matplotlib, scipy - Lichanghao Blog

    numpy, matplotlib, scipy三个包是科学计算和绘图的利器.安装它们既可以在网上下载exe安装包,也可以用python内置的包管理工具来下载安装,后者较为方便. 这几天做美赛要用到, ...

  3. TDA2050功率放大器研究

    音频功率放大模块(以下简称功放)用于处理模拟信号,将功率较低的输入信号进行线性放大,输出大功率的信号以驱动换能器.通常,电子发烧友自己设计功放,与各类音源和喇叭匹配,以得到满意的音响效果.在测试中,实 ...

  4. GoLand2019.03破解与汉化

    1.准备工作 (请认真的做好准备工作,否则中途可能会操作失败.) GoLand是JetBrains公司发布的商业版的GO语言编辑器(收费的),本屌目前还没钱购买正版,所以本次教程是以Windows平台 ...

  5. 关于js传送json到.net后台处理

    这里的内容好像跟标题不太符合,应该是如何实现将请求得到的结果作为另一个请求的请求参数,方法就是使用json处理配合全局变量进行处理 今天做项目遇到以下情景,页面请求获得一个list数据,然后要将得到的 ...

  6. AI入门之KNN算法学习

    一.什么是KNN算法 kNN(k-NearestNeighbor),也就是k最近邻算法.顾名思义,所谓K最近邻,就是k个最近的邻居的意思.也就是在数据集中,认为每个样本可以用离他最距离近的k个邻居来代 ...

  7. python读入写入中文名图片

    import os import cv2 import numpy as np # 读入中文命名图片 def cv_imread(in_path): cv_img = cv2.imdecode(np. ...

  8. 04-influxdb基本操作

    influxdb基本操作 1. 数据库基本操作 # 创建数据库 > create database db01; # 查看数据库 > show databases; name: databa ...

  9. scrapy全栈抓xpc练习

    # spider文件 # -*- coding: utf-8 -*- import scrapy import re from scrapy import Request import json im ...

  10. centos-Linux静态IP地址配置

    首先在VMware菜单中点击编辑-->虚拟网卡编辑器,查看NAT网段(子网掩码.网关.起止IP地址) 1.用nmcli命令配置IP地址 [root@Core ~]# nmcli connecti ...