JVM 基础入门 - 内存区域


Java 学习目录
上一章 JVM 基础入门 - 基础概念

运行时数据区

定义:Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域
Java 引以为豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。

所以要深入理解 JVM 必须理解内存虚拟化的概念。

在JVM 中,JVM 内存主要分为堆、虚拟机栈、本地方法栈、方法区和程序计数器等。
按照与线程的关系,也可以分为两类。

  • 线程私有区域:一个线程所单独拥有的内存区域。
  • 线程共享区域:被所有县城共享,切只有一份。

直接内存

还有就是直接内存,这个划分不是运行时数据去的一部分,但是仍会被平凡使用。
可以理解为没有被JVM锁申请管理的区域,也就是说没有被虚拟化操作系统上的内存区域。
例如:系统有8G内存 JVM 申请4G ,那么久还剩下4G,这4G 就是所谓的直接内存,可以通过JVM 的
工具去操作。工具有unsafe 类 和 Nio 使用 DirectBvteBuffer 调用直接内存。
备注:在使用直接内存时,记得一定要在使用完毕后进行回收操作,因为这部分内存不是JVM 所管理的内存空间,所以肯定会造成一定的问题。

运行时数据区

Java 方法的运行与虚拟机栈

特点:线程私有,每一个方法的执行都会生成一个栈帧,先进后出。

我们应该都知道一个线程是在多个方法之间来回执行,这也就涉及到了方法的入栈和出站。
具体应该是什么样子的呢?
JVM 基础入门 - 内存区域
上面的代码在栈中就是如下过程,从左至右,在C 方法执行完后,将在从右至左。
JVM 基础入门 - 内存区域

java 虚拟机栈的构成

JVM 基础入门 - 内存区域

虚拟机栈的大小

默认虚拟机栈的大小是1024 ,单并不一定是这个大小。不同的操作系统下,栈帧的大小是有变化的。
Oracle 中定义的各个操作系统默认虚拟机栈大小
JVM 基础入门 - 内存区域

如何设置虚拟机栈的大小呢?
-Xss 通过这个参数设置。

java 虚拟机栈中包含了栈帧
每一个栈帧中包含了局部变量表、操作数栈、动态链接、完成出口

  1. 局部变量表
    这个地方存储的是方法中的局部变量,也是栈中可能会操作或者返回的值。
  2. 操作数栈
    操作数栈是一个针对数据进行计算的地方,也就是执行引擎的工作区。
  3. 动态链接
    涉及到Java 语言特性多态,后续章节再细讲。需要结合 class 与执行引擎一起来讲。
  4. 完成出口
    正常返回:这个简单了就是记录一下完成方法后到哪里去。
    异常返回:通过异常处理器表<非栈帧中的>来确定。

程序计数器

  • 程序计数器的作用是什么呢?
    每一个线程都会有一个程序计数器, 线程间互不影响。

  • 为什么要有程序计数器?
    这就涉及到CPU 的概念了,由于Java 是多线程语言,当线程数超过 CPU 核数,线程会根据时间片轮训争夺 CPU 资源来使用,如果一个线程的时间片用完了,或者是其他原因导致这个线程的CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

    因为 JVM 是虚拟机,内部有完整的指令与执行的一套流程,所以在运行java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native方法),这个方法不是JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码执行的地址,所以在执行 native 方法时,JVM 中程序计数器的值为空(Undefined)。
    程序计数器是JVM 中唯一不会 OOM 的内存区域

栈帧执行对内存区域的影响

对 class 进行反汇编可以使用一下命令

  • javap -c xxxx.class
  • javap -v xxxx.class
    字节码助记码解释地址
    JVM 基础入门 - 内存区域
    在 JVM 中,基于解释执行的这种方式是基于栈的引擎,这个说的栈,就是操作数栈。

本地方法栈

本地方法栈跟java 虚拟机栈的功能类似,java 虚拟机栈是用来执行java 方法的调用,而本地方法栈则用户管理与本地方法的调用。本地方法是由 C 或 C++ 来实现的。因为在早起 Java 还是个菜鸡的时候,需要使用很多操作系统的功能,自己又做不到,就直接使用C 或者 C++ 已经实现的功能。

方法区

  • 用来存储已被加载的类相关的信息。
  • 包括类信息、静态变量、常量、运行时常量池、字符串常量池等。
  • 方法区可能有名字比较多。JDK 1.7 时大家习惯称之为 “永久代”,因为在 HotSpot 虚拟机中,设计人员使用了永久代来实现了JVM 规范的方法区。
  • JDK 1.8 中使用了元空间来实现了方法区。
  • 而JDK 方法区的名字是随着各个JVM 版本的实现 自己定义的,你自己可以实现一个 VM 而这个 VM 的方法区可以叫做 雷霆

符号引用

我们在开发时,肯定写过不少的 Java 类,例如 justdance.A 类简称 A 类,如果 A类引用了 justdance.B 类,在编译期的时候A 类并不知道 B 类的实际内存地址,所以在类装载器装载 A 类的时候,此时可以通过虚拟机获取 B 类的时机内存地址,因此可便可以将 justdance.B 类替换为 B 类在内存中的实际地址。

常量池与运行时常量池

在类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时常量池中,解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)
JVM 基础入门 - 内存区域
常量池包含了一下几个概念
运行时常量池、class 常量池 、字符串常量池。
静态常量池存放字符串字面量、符号引用以及类和方法的信息。
运行时常量池存放的是运行时一些直接引用。
class 常量池

元空间

方法区与堆空间类似,也是一个共享内存区,假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。
注:

  • 在 JDK 1.7 中已经将永久代的静态变量和运行时常量池转移到了堆中存储,其余的部分则存储在 JVM 的非堆内存中。
  • 在JDK 1.8 中已经将方法区的永久代去掉,取而代之的是元空间。并且源空间存储的位置是本地内存。

官方解释:移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

堆是 JVM 上最大的内存区域,我们申请的几乎所有对象,都是在这里存储的。我们常说的垃圾回收就是针对这个对空间。
堆中主要存放的有以下两种数据:

  • 基本数据类型
    byte、short、int、long、float、double、char
    存储这类数据有两个情况,一个是在方法内声明的,则直接存储在栈中,其他情况都是在堆上分配。
  • 普通对象
    JVM 会在堆中创建这个对象,然后在其他使用的地方都是它的引用。比如这个引用可以保存在栈中的局部变量表。
    堆大小参数:
    -Xms:堆的最小值;
    -Xmx:堆的最大值;
    -Xmn:新生代的大小;
    -XX:NewSize;新生代最小值;
    -XX:MaxNewSize:新生代最大值;

直接内存

直接内存也叫堆外内存,JVM 在程序运行时会申请一块内存区域,进行数据的存储,同时还有Java 虚拟机栈、本地方法栈、程序计数器,这块区域称之为栈区,从做系统剩余的内存也就是堆外内存。
他不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
如果想要使用这部分的内存区域,则需要使用使用NIO ,那样就能够操作这部分的内存区域,也可以在Java 堆内使用 directByteBuffer 对象直接引用并操作;
这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常。
单要记住在操作堆外内存时,使用过后要记得回收内存,不然问题会很难处理。