jvm内存模型

Java虚拟机内存模型

计划发布3篇博客, 这是第一篇:jvm内存模型

  • jvm内存模型
  • 对象创建和内存分配
  • OOM异常

问题

java虚拟机管理内存,无需由程序员进行内存的分配和释放。
但是如果出现内存泄漏或内存溢出,如何去排查问题?

这就需要我们去了解java虚拟机内存模型

虚拟机内存的各个区域

了解内存模型的了解虚拟机内存的各个区域

要点

  1. 各个区域的概念
  2. 各个区域的作用
  3. 各个区域的服务对象
  4. 各个区域可能会出现的问题
  5. 生命周期

运行时数据区域

  1. 程序计数器(线程私有)
  2. java虚拟机栈(线程私有)
  3. 本地方法栈(线程私有)
  4. java堆(线程共享)
  5. 方法区(线程共享)

运行时常量池:方法区一部分

直接内存:不属于jvm内存模型,但也被频繁使用,可能导致内存溢出异常

1. 程序计数器

程序计数器是什么

java虚拟机中较小的一块内存空间,是当前线程所执行的行号指示器。

每个线程都有自己的程序计数器,存储程序下一条指令的指针。

程序计数器的作用是什么

字节码解释器通过改变程序计数器中的值,来控制程序中的循环,跳转,异常处理和线程的中断和恢复。

程序计数器会发生内存溢出吗

程序计数器是java运行数据区中唯一不会发生内存溢出的区域

原因:

  1. 程序计数器存放的是每个线程程序的下一条指令指针,消耗内存小。
  2. 在程序运行时,只需改变程序计数器中指针,所以不需要额外申请内存空间。

程序计数器是线程私有的还是共有的

程序计数器是线程私有的内存

原因:

在多线程程序执行时,一个线程的程序计数器不可能被其他线程更改,否则当线程中断再恢复时无法准确的执行下一条命令。

线程私有,即生命周期与线程一致

注意

如果程序正在执行的是java方法,程序计数器记录的是:正在执行的虚拟机字节码指令的地址;

如果程序正在执行的是native方法,程序计数器的值为空:undefined

native方法:本地方法,由其他语言实现的可以在本机器执行的方法。

2. java虚拟机栈

java虚拟机栈是什么

java虚拟机栈是java方法执行的内存模型,每个方法执行时都会创建一个栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,都对应着一个栈帧在java虚拟机栈从入栈到出栈的过程。

可以这样理解:

java虚拟机栈是每个线程开始时开辟的一个栈内存空间,用于存储正在执行的方法信息

方法调用:

  1. 创建方法栈帧
  2. 栈帧入java虚拟机栈
  3. 方法执行
  4. 方法返回
  5. 栈帧出java虚拟机栈

java虚拟机栈和我们通常理解的java栈内存有什么关联

一般把java内存分为堆内存和栈内存,前者是存放java对象实例,后者存放基本数据类型信息。

实际上java栈内存指的是java虚拟机栈中,每个方法栈帧中的局部变量表部分。

什么是局部变量表

局部变量表存放的是编译期可知的各种基本数据类型和对象引用类型。

局部变量表存放各种基本类型和对象引用类型能理解,就和我们常说的java栈内存一样,什么是编译器可知?

java类在编译期时,一个方法中有多少基本数据类型和对象引用类型是可知的,而这些基本数据类型和对象引用类型的所占空间也是已知的,除了64位长度的long和double类型占用两个局部变量空间,其他的都只占用一个局部变量空间。

所以局部变量表在编译期即可确定需要分配多少内存空间。在运行期间,进入一个方法时,直接分配指定大小的内存空间,而不会改变局部变量表的大小。

即局部变量表在编译期可知,在运行期直接分配。

栈帧中除了局部变量表还有其他类型吧

栈帧是一种特殊的数据结构,存储:

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法出口

java虚拟机栈会出现内存溢出吗

会,而且分两种情况:

  1. java虚拟机栈大小不可动态扩展,当前线程请求的栈深度 > 虚拟机所允许的深度, 抛出内存溢出异常。
  2. java虚拟机栈大小可动态扩展,扩展时无法申请到足够的内存, 抛出内存溢出异常。

java虚拟机栈默认可动态扩展,可以通过参数设置。

实际案例中有因为java虚拟机栈导致的内存溢出吗

最常见的就是:死递归时,java虚拟机栈内存空间耗尽,抛出内存溢出异常。

java虚拟机栈是线程私有的,即生命周期与线程一致

3. 本地方法栈

本地方法栈是什么

本地方法栈与虚拟机栈很相似,区别就是:

  1. Java虚拟机栈是为虚拟机执行Java方法提供服务
  2. 本地方法栈是虚拟机执行native方法提供服务

这么说本地方法栈也会抛出栈溢出和内存溢出的异常?

是的,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常

注意

虚拟机规范中并没有对本地方法栈中方法所用的语言、使用方式和数据结构进行强制规定,虚拟机可以自由实现。
所以Sun HotSpot虚拟机直接把本地方法栈和虚拟机栈合而为一

4. java堆

堆我知道,就是Java存放所有对象实例的内存空间

是的,Java堆是Java虚拟机管理的最大一块内存空间,被所有线程共享,在虚拟机启用时创建。
几乎所有的对象实例都在堆上存放。

为什么说几乎

因为随着jit编译器的发展和逃逸分析技术逐渐成熟,也可能对象实例使用栈上分配和标量替换

简单说下对象实例的栈上分配和标量替换的优化技术

简单说下自己了解的,Java堆上可以分配多个线程私有的线程本地分配缓冲区(TLAB)
对象实例由对应的基本类型组成,使用逃逸分析判断是否对象是否逃逸,如果不逃逸,直接将对象所对应的基本类型在TLAB上存储。当线程结束后,TLAB上对象清理。

虚拟机栈是栈空间,那堆空间是什么样的呢

堆空间在物理上可以是不连续的,只需要在逻辑上是连续的即可,和我们本地磁盘一样。
堆空间可以实现成固定大小,也可以动态扩展,主流虚拟机都动态扩展的。

堆会发生内存溢出吗

会。如果堆中没有内存完成实例分配,而且堆无法再扩展时,就会抛出内存溢出异常

堆和垃圾收集器有什么关系

堆是Java垃圾收集器管理的主要区域,因此很多时候被称为GC堆

关于Java垃圾收集器在第三章讲解

5. 方法区

什么是方法区

和Java堆一样,是各个线程共享的内存区域,在虚拟机启动时创建。
主要存储:

  1. 被虚拟机加载的类信息
  2. 常量
  3. 静态变量
  4. 即时编译器编译后的代码

方法区会发生内存溢出吗

会。如果方法区无法满足内存分配需求时,就会抛出内存溢出异常。

而且,在低版本的虚拟机中,方法区发生内存溢出的问题更常见。

这时为什么呢

Java在1.8版本之前,使用了堆中永生代来实现了方法区。
优势:Java虚拟机可以像管理堆那样管理方法区,省去了专门为方法区编写内存管理代码的工作。
劣势:这样更容易发生内存溢出异常。因为永生代有一个内存上限,加上整个方法区占用空间,内存溢出风险大大提高。

java在1.8版本中完全移除永久代的实现,采用了metaspace(元空间)代替。元空间不在虚拟机中,而是使用本地内存。

运行时常量池

运行时常量池是什么

运行时常量池是方法区的一部分。
方法区存储被虚拟机加载的类信息,而Class文件中有:

  1. 类的版本号
  2. 字段
  3. 方法
  4. 接口
  5. 常量池

常量池存放:编译期生成的各种字面量和符号引用

而常量池在运行时会被放到运行时常量池中。

什么是字面量和符号引用

字面量:String a = "hello"; 字符串"hello" 就是字面量
符号引用:存在于class文件中,运行时经过动态链接变成对象的直接引用。因为java的多态性,在运行时确定具体的引用对象,所以编译期的是符号引用,被虚拟机严格规定的对象引用字面量。

运行时常量池会发生内存溢出吗

运行时常量池是方法区的一部分,所以和方法区一致,当常量池无法再申请到内存时,抛出内存溢出异常

常量池、运行时常量池、字符串常量池这三个都弄糊涂了

  1. 常量池:即class文件常量池,是class文件的一部分,用于保存编译时确定的数据。
  • 保存的内容:
      1. 字面量
        1. 文本字符串
        1. 被声明为final的常量值
        1. 基本数据类型的值
        1. 其他
      1. 符号引用
        1. 类和结构的完全限定名
        1. 字段名称和描述符
        1. 方法名称和描述符
  1. 运行时常量池:

      1. java不一定在编译期产生,运行期也可能产生常量,例如使用String.intern()方法。这些常量被放到运行时常量池中。
      1. 类加载后,常量池中数据也会在运行时常量池中存放
  2. 字符串常量池:在JDK1.7之前的HotSpot JVM中,记录interned String的一个全局表 StringTable, 本质是HashSet。他只存储Java.lang.String的引用,而不记录String的内容。

  • 全局共享,只有一份。1.8之后从永生代移到java堆中

java1.8方法区变化

  1. 移除了永生代,使用元空间实现方法区。
  2. 永久代的class metadata,即被虚拟机加载的类信息,移到元空间
  3. 永久代的字符串常量池和静态变量,移到java堆中
  4. 永久代参数 -> 元空间参数

关于java1.8的内存模型,参考:

java8内存模型-永久代和元空间

直接内存

直接内存是什么,不在java虚拟机五个数据区域里呀

是的,直接内存并不属于java虚拟机管理的五个数据区域。但也是经常用到的,也可能会发生内存溢出异常。

在jdk1.4中引入了NIO类,使用基于通道和缓冲区的I/O方式。
它是使用Native函数库直接分配堆外内存,然后通过存储在堆中的DirectByteBuffer对象,作为这块内存的引用进行操作。
这样在一些场景中可以显著的提高性能,因为避免了在Java堆和Native堆中来回复制数据。

既然直接内存是堆外内存,不受java堆大小的限制,为什么还会发生内存溢出呢?

因为它还是内存空间,会受到本机系统的内存大小限制。
如果虚拟机各个内存区域总和 > 物理内存限制,就很可能导致直接内存动态扩展时出现内存溢出异常。

总结

java虚拟机内存模型

1. java虚拟机5个内存区域

  1. 程序计数器
  • 生命周期:线程私有,随线程存在
  • 存储内容:存储当前线程下一条指令的引用
  • 内存溢出异常:没有内存溢出异常
  1. java虚拟机栈
  • 生命周期:线程私有,随线程存在
  • 存储内容:存储当前线程正在执行方法的栈帧
  • 内存溢出异常:
      1. 虚拟机栈不可扩展,栈请求的深度 > 虚拟机允许的栈深度
      1. 虚拟机栈可扩展,扩展时无法申请到足够的内存
  1. 本地方法栈
  • 生命周期:线程私有,随线程存在
  • 存储内容:存储当前线程正在执行的本地方法的栈帧
  • 内存溢出异常:
      1. 本地方法栈不可扩展,栈请求的深度 > 虚拟机允许的栈深度
      1. 本地方法栈可扩展,扩展时无法申请到足够的内存
  1. java堆
  • 生命周期:线程共享,虚拟机启动时创建
  • 存储内容:几乎所有的java对象实例
  • 内存溢出异常:堆中没有内存空间完成对象内存分配,且扩展时无法申请到足够的内存
  1. 方法区
  • 生命周期:线程共享,虚拟机启动时创建
  • 存储内容:被虚拟机加载的类信息、静态变量、常量
  • 内存溢出异常:无法申请到足够的内存

java虚拟机管理之外的内存区域

直接内存

  • 生命周期::线程共享,虚拟机启动时创建
  • 存储内容:Java NIO的Channel和Buffer
  • 内存区域:使用Native函数库直接分配堆外内存
  • 内存溢出异常:本地内存用完

java1.8方法区变化

  1. 移除了永生代,使用元空间实现方法区。
  2. 永久代的class metadata,即被虚拟机加载的类信息,移到元空间
  3. 永久代的字符串常量池和静态变量,移到java堆中
  4. 永久代参数 -> 元空间参数

想共同学习jvm的可以加我微信:1832162841,或者进QQ群:982523529

你可能感兴趣的:(jvm内存模型)