前言

首先JVM内存结构和JAVA内存模型是两种东西

JVM内存结构:通常被叫做JVM内存模型,也叫做Java内存区域,Java运行时数据区。

JAVA内存模型:是JMM(Java Memory Model,简称 JMM)。是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存中的工作方式。理解好 Java 内存模型,对于我们想深入了解 Java并发编程有先导作用

 

JVM内存结构

通过前篇学习了解,JVM可以将class 文件解释成各个平台可以识别的机器码,最终实现跨平台运行代码,这也是为什么需要JVM。

对于JVM内存模型,大体可以分为两个部分。

  • 线程私有:虚拟机栈,本地方法栈,程序计数器
  • 线程公有:堆,方法区

下面展开解释五大组成部分的作用




Java内存模型规定了一个线程何时 java的内存模型和内存结构_java


堆(Heap)

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。


Java内存模型规定了一个线程何时 java的内存模型和内存结构_jvm_02


Java堆在虚拟机启动的时候就被创建,Java堆主要用来为类实例对象和数组分配内存,也是JVM所管理内存中最大的一块区域。Java堆的划分目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。

堆大小可以通过 -Xms (最小值) 和-Xmx(最大值)进行设置且最大最小值都要小于1G。

  • -Xms 为启动时申请的最小内存,默认是操作系统物理内存的1/64
  • -Xmx 为可申请的最大物理的1/4

默认当空余堆内存小于40%时,JVM会增大堆内存到-Xmx指定的大小,可通过

-XX:MinHeapFressRation= 来指定这个比列。当空余堆内存大于70%时,JVM会减小堆内存的大小到-Xms指定的大小,可通过-XX:MaxHeapFreeRation= 来指定这个比列,当然为了避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

从GC的角度可以将JVM分为新生代、老年代和永久代。其中新生代默认占1/3堆内存空间,老年代默认占2/3堆内存空间,永久代占非常少的对内存空间。所以新生代:老年代 = 1:2

新生代又分为Eden区、SurvivorFrom区和SurvivorTo区, Eden区默认占8/10新生代空间,SurvivorFrom区和SurvivorTo区默认分别占1/10新生代空间,Eden区最小占3/5新生代空间,SurvivorFrom区和SurvivorTo区分别占1/5新生代空间。 Eden : From : To = 8 : 1 : 1。

正是因为堆是最大的内存管理区域,所以堆也是垃圾回收(GC)的主要发生地,也被常称为GC堆。主要发生两种GC: Minor GC、Full GC(也叫做Major GC)。

  • Minor GC

具体实现:

  • 把在Eden区和SurvivorFrom区中存活的对象复制到SurvivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由 XX:MaxTenuringThreshold设置,默认为15),则将其复制到老年代,同时把这些对象的年龄加1;

大小为2KB-128KB的对象属于大对象,例如通过 XX:PretenureSizeThreshold=2097152 设置大对象为2MB,1024 × 1024 × 2Byte = 2MB),则也直接将其复制到老年代。

  • 清空Eden区和SurvivorFrom区中的对象。
  • 将SurvivoTo区和SurvivorFrom区互换,原来的SurvivorTo区成为下一次GC时的SurvivorFrom区。
  • Full GC

Java堆可能发生如下异常情况:如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。简称(OOM)。

永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。Class在类加载时被放入永久。永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载Class文件过多时会抛出Out Of Memory异常,比如Tomcat引用Jar文件过多会导致JVM内存不足而无法启动。

需要注意的是,在Java 8 中永久代已经被元数据区(也叫做元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,之和操作系统的内存有关。

在Java 8 中,JVM将类的元数据放入本地内存(Native Memory)中,将常量池和类的静态变量放入Java堆中,这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermSize)空间决定,而由操作系统的实际可用的内存空间决定。

方法区(Method Area 元空间 )

方法区在虚拟机启动的时候被创建,存储了每一个类的结构信息。如运行时常量池,字段,方法数据,构造函数,和普通的字节码内容,还包括在类,实例,接口初始化时用到的特殊方法。


Java内存模型规定了一个线程何时 java的内存模型和内存结构_Java内存模型规定了一个线程何时_03


如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常.

虚拟机栈(JVM Stack)


Java内存模型规定了一个线程何时 java的内存模型和内存结构_Powered by 金山文档_04


虚拟机栈,早期也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。

作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

  • 每个方法被执行的时候都会创建一个”栈帧”,用于存储局部变量表(包括参数)、操作栈、方法出口等信息。
  • 栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈的基本元素,栈帧由局部变量区、操作数栈等组成。
  • 每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,该线程中,虚拟机有且也只会对当前栈帧进行操作。

问题:JVM规定JAVA栈的大小是动态或者固定不变的

  • 虚拟机栈动态扩展 在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,则会抛出 OutOfMemoryError
  • 虚拟机栈固定大小 每个线程的虚拟机栈容量可以在线程创建的时候独立选定,若是线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,则会抛出 StackOverflowError异常

使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

本地方法栈(Natice Stack)

本地方法栈(Native Method Stack)与虚拟机栈作用大致相同。区别是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native 方法服务。

同样会抛出和虚拟机栈想同的OutOfMemoryError 异常 和StackOverflowError异常


Java内存模型规定了一个线程何时 java的内存模型和内存结构_java_05


该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。

Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。

程序计数器(PC Register)

特点:

  • 线程隔离性,每个线程工作时都有属于自己的独立计数器。
  • 执行Java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址。在进行线程切换的时候能够保证线程的正确运行。
  • 执行Native本地方法时,程序计数器的值为空(Undefined)。因为Native方法是java通过JNI直接调用本地C/C++库,可以近似的认为Native方法相当于C/C++暴露给Java的一个接口,Java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是Java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
  • 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
  • 程序计数器,是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

摘录

JAVA 内存模型

定义说明

Java内存模型(Java Memory Model简称JMM) 是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量。

工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。


Java内存模型规定了一个线程何时 java的内存模型和内存结构_Powered by 金山文档_06


主内存与工作内存的同步

数据同步的八大原子操作

  • lock(锁定) :作用于主内存的变量,把一个变量标记为一条线程独占状态
  • unlock(解锁) :作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取) :作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入) :作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用) :作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  • assign(赋值) :作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  • store(存储) :作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  • write(写入) :作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中。

如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但 Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。


Java内存模型规定了一个线程何时 java的内存模型和内存结构_Powered by 金山文档_07


同步规则

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

摘录

JAVA内存模型特征

多线程下保证解决 原子性,可见性,有序性问题

  • 原子性
  • 可见性 volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。
  • 有序性

重排序和happens-before规则

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

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行的重排序。
  • 现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。


Java内存模型规定了一个线程何时 java的内存模型和内存结构_Powered by 金山文档_08


happens-before 原则 从JDK 5开始,Java使用新的JSR-133内存模型,提供了 happens-before 原则 来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据

  • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  • volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  • 传递性 A先于B ,B先于C 那么A必然先于C
  • 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 对象终结规则对象的构造函数执行,结束先于finalize()方法

摘录

总结

JVM内存结构主要分为五大区 堆,方法区,虚拟机栈,本地方法栈,程序计数器。理解堆的作用,包括其内部的组成和使用,了解JVM各部分在线程执行时所参与的步骤与功能

JAVA内存模型,可以理解为一种抽象的概念与规则。通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式, JMM是围绕原子性,有序性、可见性展开 。后续加深对于Volatile关键字的理解,明白其作用,合理使用,过后对于并发编程的学习理解有着推进作用