首先JVM内存结构和JAVA内存模型是两种东西
:通常被叫做JVM内存模型,也叫做,。
:是JMM()。是定义了线程和主内存之间的抽象关系,即 。理解好 Java 内存模型,对于我们想深入了解 Java并发编程有先导作用
通过前篇学习了解,JVM可以将class 文件解释成各个平台可以识别的机器码,最终实现跨平台运行代码,这也是为什么需要JVM。
对于JVM内存模型,大体可以分为两个部分。
- :虚拟机栈,本地方法栈,程序计数器
- :堆,方法区
下面展开解释五大组成部分的作用
在 Java 中,堆被划分成两个不同的区域:,新生代 ( Young ) 又被划分为三个区域:。
Java堆在虚拟机启动的时候就被创建,Java堆主要用来为,也是JVM所管理内存中最大的一块区域。Java堆的划分目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。
堆大小可以通过 -Xms (最小值) 和-Xmx(最大值)进行设置且最大最小值都要小于1G。
- -Xms 为启动时申请的最小内存,默认是操作系统物理内存的1/64
- -Xmx 为可申请的最大物理的1/4
默认当空余堆内存小于40%时,JVM会增大堆内存到-Xmx指定的大小,可通过
来指定这个比列。当空余堆内存大于70%时,JVM会减小堆内存的大小到-Xms指定的大小,可通过来指定这个比列,当然为了避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
从GC的角度可以将JVM分为新生代、老年代和永久代。其中新生代默认占1/3堆内存空间,老年代默认占2/3堆内存空间,永久代占非常少的对内存空间。所以
新生代又分为Eden区、SurvivorFrom区和SurvivorTo区, Eden区默认占8/10新生代空间,SurvivorFrom区和SurvivorTo区默认分别占1/10新生代空间,Eden区最小占3/5新生代空间,SurvivorFrom区和SurvivorTo区分别占1/5新生代空间。
正是因为堆是最大的内存管理区域,所以堆也是垃圾回收(GC)的主要发生地,也被常称为GC堆。主要发生两种GC: Minor GC、Full GC(也叫做Major GC)。
- 主要发生在新生代,采用复制算法收集。由于JVM的频繁创建对象,所以新生代会频繁触发 Minor GC
具体实现:
- 把在Eden区和SurvivorFrom区中存活的对象复制到SurvivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由 设置,默认为),则将其复制到老年代,同时把这些对象的年龄加1;
如果SuriviorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(),则也直接将其复制到老年代。
- 清空Eden区和SurvivorFrom区中的对象。
- 将SurvivoTo区和SurvivorFrom区互换,原来的SurvivorTo区成为下一次GC时的SurvivorFrom区。
- 主要发生在老年代,较为稳定,不会频繁触发,采用标记清除算法
Java堆可能发生如下异常情况:如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。简称(OOM)。
方法区在虚拟机,存储了每一个类的结构信息。如。
如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常.
虚拟机栈,早期也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
- 每个方法被执行的时候都会创建一个”栈帧”,用于信息。
- 栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈的基本元素,栈帧由局部变量区、操作数栈等组成。
- 每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,该线程中,虚拟机有且也只会对当前栈帧进行操作。
问题:JVM规定JAVA栈的大小是动态或者固定不变的
- 在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,则会抛出 异常
- 每个线程的虚拟机栈容量可以在线程创建的时候独立选定,若是线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,则会抛出 异常
本地方法栈(Native Method Stack)与虚拟机栈作用大致相同。区别是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native 方法服务。
同样会抛出和虚拟机栈想同的 异常 和异常
该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
:
- 线程隔离性,每个线程工作时都有属于自己的独立计数器。
- 执行Java方法时,。在进行线程切换的时候能够保证线程的正确运行。
- 执行Native本地方法时,程序计数器的值为空(Undefined)。因为Native方法是java通过JNI直接调用本地C/C++库,可以近似的认为Native方法相当于C/C++暴露给Java的一个接口,Java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是Java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
- 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
- 。
Java内存模型() 是一种抽象的概念,并不真实存在,它。通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量。
- lock(锁定) :作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁) :作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取) :作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入) :作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用) :作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值) :作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
- store(存储) :作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
- write(写入) :作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中。
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但 Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
多线程下保证解决
- 除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过 synchronized 和Lock 实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。
- volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。
- 通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述volatile关键字)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行的重排序。
- 现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从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内存模型,可以。通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式, 。后续加深对于关键字的理解,明白其作用,合理使用,过后对于并发编程的学习理解有着推进作用
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/3509.html