写点什么

Java 内存区域和内存模型

  • 2022 年 5 月 10 日
  • 本文字数:3088 字

    阅读完需:约 10 分钟

Java Virtual Machine Stacks:与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。


虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表操作栈动态链接方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。


栈是一个先入后出(FILO-First In Last Out)的有序列表。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。


[](()局部变量表

局部变量表是存放方法参数局部变量的区域。局部变量没有准备阶段,必须显式初始化。全局变量是放在堆的,有两次赋值的阶段,一次在类加载的准备阶段,赋予系统初始值;另外一次在类加载的初始化阶段,赋予代码定义的初始值。

[](()操作栈

操作栈是个初始状态为空的桶式结构栈(先入后出)。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

[](()动态链接

每个栈帧都包含一个指向运行时常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。当前方法中如果需要调用其他方法的时候,能够从运行时常量池中找到对应的符号引用,然后将符号引用转换为直接引用,然后就能直接调用对应方法。


不是所有方法调用都需要动态链接的,有一部分符号引用会在类加载解析阶段将符号引用转换为直接引用,这部分操作称之为: 静态解析,就是编译期间就能确定调用的版本,包括: 调用静态方法, 调用实例的私有构造器, 私有方法,父类方法。

[](()方法返回地址

方法执行时有两种退出情况:


  1. 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;

  2. 异常退出。


无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧。

[](()堆

我们经常说的 GC 调优/JVM 调优,99%指的都是调堆!Java 栈、本地方法栈、程序计数器这些一般不会产生垃圾。


Heap:Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。


堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage Collected Heap)。

[](()元空间

JDK 1.8 就把方法区改用元空间了。类的元信息被存储在元空间中,元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大。

[](()方法区

Method Area:与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。


虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

[](()方法区与元空间的变迁

下图是 JDK 1.6、JDK 1.7 到 JDK 1.8 方法区的大致变迁过程:



JDK 1.8 中 HotSpot JVM 移出 永久代(PermGen),开始时使用元空间(Metaspace)。使用元空间取代永久代的实现的主要原因如下:


  1. 避免 OOM 异常,字符串存在永久代中,容易出现性能问题和内存溢出;

  2. 永久代设置空间大小是很难确定,太小容易出现永久代溢出,太大则容易导致老年代溢出;

  3. 永久代进行调优非常困难;

  4. 将 HotSpot 与 JRockit 合二为一;


[](()内存模型




内存模型是为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。


内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障


Java 内存模型(JMM)控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。

[](()计算机高速缓存和缓存一致性

计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存和处理器之间的缓冲。当程序在运行过程中,会将运算需要的数据从主存(计算机的物理内存)复制一份到 CPU 的高速缓存当中,那么 CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。


在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己的高速缓存,它们有共享 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 同一主内存(Main Memory)。当 CPU 要读取一个数据时,首先从一级缓存中查找,如果没有找到,再从二级缓存中查找,如果还是没有就从三级缓存(不是所有 CPU 都有三级缓存)或内存中查找。



在多核 CPU 中,每个核在自己的缓存中,关于同一个数据的缓存内容可能不一致。为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。

[](()JVM 主内存与工作内存

Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。


这里的工作内存是 JMM 的一个抽象概念,其存储了该线程以读 / 写共享变量的副本


[](()重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:


  1. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。


从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:



JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。


Java 编译器禁止处理器重排序是通过在生成指令序列的适当位置会插入内存屏障(重排序时不能把后面的指令重排序到内存屏障之前的位置)指令来实现的。

[](()happens-before

Java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。


如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

[](()Java 内存模型的实现

在 Java 中提供了一系列和并发处理相关的关键字,比如 volatilesynchronizedfinalJUC 包等。其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字。

[](()原子性

为了保证原子性,提供了两个高级的字节码指令 monitorentermonitorexit,这两个字节码,在 Java 中对应的关键字就是 synchronized


我们对 synchronized 关键字都很熟悉,你们可以把下面的代码编译成 class 文件,用 javap -v SyncViewByteCode.class 查看字节码,就可以找到 monitorentermonitorexit 字节码指令。


public class SyncViewByteCode {


public synchronized void buy() {


System.out.println("buy porsche");


}


}


字节码,部分结果如下:


public com.dolphin.thread.locks.SyncViewByteCode();


descriptor: ()V


flags: ACC_PUBLIC


Code:


stack=1, locals=1, args_size=1


0: aload_0

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Java内存区域和内存模型_Java_爱好编程进阶_InfoQ写作社区