JVM内存模型|8月更文挑战

204 阅读7分钟

JVM试图定义一种统一的内存模型,能将各种底层硬件以及操作系统的内存访问差异进行封装,使Java程序在不同硬件以及操作系统上都能达到相同的并发效果。它分为工作内存和主内存,线程无法对主存储器直接进行操作,如果一个线程要和另外一个线程通信,那么只能通过主存进行交换。如下图所示:

JVM内存结构(JDK1.6).png

JVM内存结构(JDK1.7).png

JVM内存结构(JDK1.8).png

线程隔离数据区:

  • 程序计数器: 当前线程所执行字节码的行号指示器
  • 虚拟机栈: 里面的元素叫栈帧,存储局部变量表、操作栈、动态链接、方法出口等,方法被调用到执行完成的过程对应一个栈帧在虚拟机栈中入栈到出栈的过程
  • 本地方法栈: 和虚拟机栈的区别在于虚拟机栈为虚拟机执行Java方法,本地方法栈为虚拟机使用到的本地Native方法服务

线程共享数据区:

  • 方法区: 可以描述为堆的一个逻辑部分,或者说使用永久代来实现方法区。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 堆: 唯一目的就是存放对象的实例,是垃圾回收管理器的主要区域,分为Eden、From/To Survivor空间

1 程序计数器

程序计数器(Program Counter Register)。一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

  • 线程私有
  • 是一块很小的独立内存空间
  • 主要存储当前线程所执行的字节码行号指示器
  • 以一种数据结构的形式放置于内存中
  • 分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成

注意:此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2 JAVA虚拟机栈

JAVA虚拟机栈(Java Virtual Machine Stacks)。是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

  • 线程私有
  • JAVA线程创建同时,会自动创建对应的JAVA栈
  • JAVA栈包含多个栈帧(运行每个方法,就会自动创建一个栈帧,用于存储局部变量、操作栈和返回值等)

相关参数:

-Xss:设置方法栈的最大值

3 本地方法栈

本地方法栈(Native Method Stacks)。本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

  • 线程私有
  • 与JAVA栈的作用相似
  • 主要为JVM使用本地方法(native)提供支持
  • 不是由Java实现的,而是由C实现的

与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常:

 // 原因:对象不能被分配到堆内存中
 Exception in thread "main": java.lang.OutOfMemoryError: Java heap space
 // 原因:类或者方法不能被加载到持久代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库
 Exception in thread "main": java.lang.OutOfMemoryError: PermGen space
 // 原因:创建的数组大于堆内存的空间
 Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit
 // 原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间
 Exception in thread "main": java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
 // 原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现
 Exception in thread "main": java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)

4 方法区

方法区(Method Area)。即我们常说的永久代(Permanent Generation) , 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

  • 又称之为:非堆(Non-Heap)永久区
  • 线程共享
  • 主要存储:类的类型信息、常量池(Runtime Constant Pool) 、字段信息、方法信息、类变量和Class类的引用等
  • Java虚拟机规范规定:当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

相关参数:

-XX:PermSize:设置Perm区的初始大小

-XX:MaxPermSize:设置Perm区的最大值

5 堆内存

堆内存(JAVA Heap)。是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden区、From Survivor 区和 To Survivor 区)和老年代。

  • 线程共享
  • 主要用于存储JAVA实例或对象
  • GC发生的主要区域
  • 是Java虚拟机所管理的内存中最大的一块
  • 当堆中没有内存能完成实例分配,且堆也无法再扩展,则会抛出OutOfMemoryError异常

相关参数:

-Xms:设置堆内存初始大小

-Xmx:设置堆内存最大值

-XX:MaxTenuringThreshold:设置对象在新生代中存活的次数

-XX:PretenureSizeThreshold:设置超过指定大小的大对象直接分配在旧生代中

新生代相关参数(注意:当新生代设置得太小时,也可能引发大对象直接分配到旧生代):

-Xmn:设置新生代内存大小

-XX:SurvivorRatio:设置Eden与Survivor空间的大小比例