JVM内存分区和Java内存模型(Java Memory Model)

概念

  • JVM内存分区具体指的是JVM中运行时数据区的分区。
  • JMM是一种规范,是抽象的概念,目的是解决由于多线程并发编程通过内存共享进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题,即保证内存共享的正确性(可见性、有序性、原子性)。
  • Java内存分区和JMM是完全不同层次的概念,更恰当说JMM描述的是一组规范,围绕原子性,有序性、可见性,通过这组规范控制程序中各个变量在共享数据区域和私有数据区域的访问方式。JMM与Java内存区域其实都是抽象的概念,唯一相似点,都存在共享数据区域和私有数据区域。在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。

JVM内存区域划分

在这里插入图片描述
老生常谈的问题:.java文件被Java Compiler编译为.class字节码文件,随后Class loader加载各类的字节码文件,加载完后交由Execution Engine执行。执行引擎负责具体的代码调用及执行过程。就目前而言,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件、处理过程是等效字节码解析过程,输出的是执行结果。Runtime Data Area用来存放程序运行时的数据和相关信息,也就是常说的JVM内存。

Runtime Data Area

在这里插入图片描述
JVM规范了内存分区,由方法区、堆、虚拟机栈、程序计数器、本地方法栈组成。

  • 方法区(Mehtod Area):属线程共享内存区域,作用是储存已被JVM加载的类信息、常量、静态变量、即时编译后的代码等。它是堆的一个逻辑部分,为了与堆区分开,又叫Non-Heap,相对而言,GC对于这个区域的收集是很少出现的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

    • 其中的Runtime Constant Pool(运行时常量池), 用于存放编译器生成的符号引用和字面量(就是这个量本身,如字符串“ “ABC” ”,int型"3"),由于Java不要求常量一定在编译时产生,所以它具备 动态性 特征,运行期间产生的新常量也会加入池中。
  • Java堆(Heap):属线程共享内存区域,在虚拟机启动时创建,占用区域最大,用于存放对象实例,所有对象实例和数组都要在堆上分配内存,可以处理不连续的内存空间,可扩展,是GC机制管理的主要区域,所以也被叫做GC堆。当堆中没有内存满足实例分配需求,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
    为了支持垃圾收集,堆被分为三个部分:

    • 年轻代 : 常常又被划分为Eden区和Survivor(From Survivor To Survivor)区(Eden空间、From Survivor空间、To Survivor空间(空间分配比例是8:1:1)
    • 老年代
    • 永久代 (jdk 8已移除永久代)
  • 虚拟机栈(JVM Stacks):属线程私有内存区域, 也是常说的栈。Java栈是Java方法执行的内存模型。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当虚拟机栈中没有内存满足实例分配需求,会抛出StackOverflowError和OutOfMemoryError异常。

  • 程序计数器(Program Counter Register):属线程私有内存区域,占一小块内存区域,用于指示当前执行字节码的行号,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,此内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。

  • 本地方法栈(Native Method Stacks):属线程私有内存区域,本地方法栈与虚拟机栈发挥的功能非常类似,只是虚拟机栈为虚拟机执行java方法而服务,而本地方法栈为虚拟机执行native方法而服务。当本地方法栈中没有内存满足实例分配需求,会抛出StackOverflowError和OutOfMemoryError异常。

Java内存模型(JMM)

JMM是一种抽象的概念,它是一种规范,定义了程序中各个变量访问的方式。JVM运行程序的实体是线程,每个线程创建时JVM会为其创建相应的工作内存(空栈间),用于储存线程私有数据,JMM中规定所有变量都存储在主内存上,所有线程都可访问,线程对于变量的操作(赋值、读取等)必须在工作内存进行,操作完成写回主内存,这样各线程之间就无法相互访问,线程间的通信(传值)必须通过主内存来完成。

  • 主内存(堆内存):主要存储实例对象,所有线程创建的实例对象(成员、局部、静态、常量等)都放在主内存中。存在线程安全问题(造成主内存与工作内存间数据存在一致性问题)。
  • 工作区域(私有线程域):主要存储当前方法的所有本地变量信息(主内存中变量的复制,也包含字节码行号指示器、相关Native方法信息)。线程中的本地变量对其他线程不可见,不存在线程安全问题。

主内存与工作内存的数据存储类型、操作方式及与硬件的关系

如果方法中的数据是基本数据类型,将直接存储在栈帧结构中;如果本地变量是引用类型,那么该引用会存储在工作内存的栈帧中,而对象实例还是会存在主内存(堆)中。对于实例对象的成员变量,无论类型都被存在堆中。当两个线程同时调用了一个对象的同一个方法时,两条线程都会将所涉及的数据复制一份到自己的工作内存中,操作完成后刷新到主内存中。JMM是一种抽象的概念,并不实际存在,在逻辑上分工作内存和主内存,但在物理上二者都可能在主存中也可能在Cache或者寄存器中。Java内存分区也是这个道理。

Java线程的实现原理

在Windows和Linux系统上,Java线程实现是基于一对一的线程模型,即通过语言级的程序(JVM)去间接地调用操作系统内核的线程模型。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个CPU各个核心进行并发执行,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。
在这里插入图片描述
关于其中的内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。

并发编程的问题

多线程并发编程会涉及到以下的问题:

  • 原子性:指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行,多线程中为了提高性能,编译器和处理器的常常会对指令做重排(编译器优化重排、指令并行重排、内存系统重排)。

JMM的具体实现

JMM还提供了一系列原语(由若干条指令组成的,用于完成一定功能的一个过程),封装了底层实现。

  • 原子性:Java提供了两个高级字节码指令monitorenter和monitorexit,对应的是关键字synchronized,使用该关键字保证方法和代码块内的操作的原子性。

  • 可见性:Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
    除了volatile,Java中的synchronized和final两个关键字也可以实现可见性,只不过实现方式不同。

  • 有序性:用volatile关键字禁止指令重排,用synchronized关键字加锁。

转载:https://blog.csdn.net/qq_41297896/article/details/89949632

发布了74 篇原创文章 · 获赞 23 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qxhly/article/details/104637822