[TOC]

概述

文章参考:https://blog.csdn.net/javazejian/article/details/72772461

首先,我们先来思考几个问题:

  • 1、Java的内存结构总共分为几个部分?
  • 2、Java的内存结构中这些内存区域哪些是线程私有的?哪些是线程共有的?
  • 3、Java中内存结构中有哪些区域可能会发生OOM?

在说Java内存模型之前,我们先说一下Java的内存结构,也就是运行时的数据区域:
  Java虚拟机在执行Java程序的过程中,会把它管理的内存划分为几个不同的数据区域,这些区域都有各自的用途、创建时间、销毁时间。
下面这张图,是我从网上抠下来的。大家可以看看。

image-20221210145202060

Java运行时数据区分为下面几个内存区域:  

1.PC寄存器/程序计数器(也就是图中右下角部分):

为了保证程序能够连续地执行下去,处理器必须具有某些手段来确定下一条指令的地址,而程序计数器正是起到这种作用。

程序计数器严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址。程序计数器是一个较小的内存空间,线程私有,它是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域。

由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址(也就是当前方法的JVM指令地址)必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。

为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为”线程私有内存区域”,这在某种程度上有点类似于“ThreadLocal”,所以程序计数器是线程私有的,是线程安全的。

如果线程执行的方法不是Native方法,则程序计数器保存正在执行的字节码指令地址,如果是Native 方法则程序计数器的值为空(Undefined)。

2.Java虚拟机栈 Java Stack(图中中间部分):

虚拟机栈和程序计数器一样,同样为线程所私有,并且生命周期和线程相同。

Java虚拟机栈存储线程中Java方法调用的状态,包括局部变量、参数、返回值以及运算的中间结果等。每个栈中的数据都是私有的,其他栈不允许访问。虚拟机栈主要存放各种编译期可知的基本数据类型和对象的引用。

所以Java虚拟机栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈、动态链接、和方法返回值等信息。

每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。

只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。

由于Java栈是与线程对应起来的,所以Java虚拟机栈数据的内存区域也是“线程私有内存区域”,所以不需要关心其数据一致性,也不会存在同步锁的问题。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

image-20221210145328061

3.本地方法栈Native Method Stack(图中右上角):

Java虚拟机实现可能要用到C Stacks来支持Native语言,这个C Stacks就是本地方法栈(Native Method Stack)。

本地方法栈与Java虚拟机栈类似,只不过本地方法栈是用来支持Native方法的。如果Java虚拟机不支持Native方法,并且也不依赖于C Stacks,可以无须支持本地方法栈。在Java虚拟机规范中对本地方法栈的语言和数据结构等没有强制规定,因此具体的Java虚拟机可以自由实现它,比如HotSpot VM将本地方法栈和Java虚拟机栈合二为一。

与Java虚拟机栈类似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。   

4.堆 Heap:

在C语言中,程序员可以通过malloc函数和free函数在堆上申请和释放空间。

那么在Java中是怎么样的呢?Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的),几乎所有的对象实例都在这里分配内存。Java堆内存是被所有线程共享的运行时内存区域。
 
堆是JVM所管理的内存中最大的一块,并且它可以处于物理上不连续的内存空间中。是被所有Java线程锁共享的,所以它属于线程共有内存区域,不是线程安全的,在JVM启动时创建,并且被所有线程所共享。

堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:几乎所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域,在JVM中只有一个堆。

从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

Java 虚拟机规范中定义了一种异常情况:如果在堆中没有足够的内存来完成实例分配,并且堆也无法进行扩展时,则会抛出OutOfMemoryError异常。

5.方法区Method Area:

方法区和 Java 堆一样,是各个线程共享的内存区域,主要存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这个区域的内存回收目标主要是针对常量池的回收和对类型的写在,较少发生垃圾收集行为。

方法区存放了要加载的类的结构信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。

方法区是被Java线程锁共享的,属于线程共有内存区域,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

方法区是Java堆的逻辑组成部分,它一样在物理上不需要连续,并且可以选择在方法区中不实现垃圾收集。方法区并不等同于永久代,只是因为HotSpot VM 使用永久代来实现方法区,对于其他的Java虚拟机,比如J9和JRockit等,并不存在永久代概念。

6.常量池Constant Pool:

运行时常量池(Runtime Constant Pool)并不是运行时数据区域的其中一份子,而是方法区的一部分。常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。

Class文件不仅包含类的版本、接口、字段和方法等信息,还包含常量池,它用来存放编译时期生成的字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中。运行时常量池可以理解为是类或接口的常量池的运行时表现形式。

在Java虚拟机规范中定义了一种异常情况:当创建类或接口时,如果构造运行时常量池所需的内存超过了方法区所能提供的最大值,Java虚拟机会抛出OutOfMemoryError异常。

好了。上面就是我们讲解了Java运行时内存区域的六大模块区域。其实有的时候,我们很多时候简单粗暴的把Java的内存分为堆内存和栈内存。当然。这样简单粗暴的分类不是真准确的。但是却是有依据的,原因就是我们上面说到的。方法区和常量池都是堆内存的组成部分。

Java运行时内存区域的线程机制

在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步?
在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。

所以Java中的内存区域主要分为两类:

线程私有内存区域和线程共有内存区域。

  • 线程私有内存区域:程序计数器、JVM 虚拟机栈、本地方法栈
  • 线程共有内存区域:堆、方法区、运行时常量池

线程之间通过共享线程共有的区域,通过读-写内存中公共状态的方式来进行隐式的通信。同步指的是程序在控制多个线程之间执行程序的相对顺序的机制,在共享内存模型中,同步是显式的,程序员必须显式指定某个方法/代码块需要在多线程之间互斥执行。   

现在我们来思考第三个问题:哪些区域可以发生OOM??

其实,除了程序计数器,其他的部分都会发生 OOM。

堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏。

JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的。

方法区。方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的。

image-20221210145358750