码迷,mamicode.com
首页 > 其他好文 > 详细

JVM内存模型

时间:2020-08-27 17:11:42      阅读:67      评论:0      收藏:0      [点我收藏+]

标签:基本数据   select   sel   contain   根据   attr   并发编程   perm   入栈   

内存模型

方法区(Method Area):
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。
方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用。符号引用包括:1.类的全限定名,2.字段名和属性,3.方法名和属性。
JVM堆(Java Heap):
Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
程序计数器(Program Counter Register):
字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
多线程中,为了让线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响、独立存储,因此这块内存是线程私有的。
虚拟机栈(Java Virtual Machine Stacks):
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链表、方法出口信息等。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈(Native Method Stacks):
本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,一般情况下,我们无需关心此区域。

JVM内存布局图:

技术图片技术图片?

Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

 

 

方法区

  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载
  • 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

本地方法栈

     而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

  • 本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。
  • 不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。
  • HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。

JVM Stack(虚拟机栈)

栈是一个先进后出的数据结构。

JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。

栈中的数据都是以栈帧的格式存在,每个方法执行的同时都会创建一个栈帧,方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。

在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,方法执行完会跳转到另一个栈帧上,栈帧是方法运行的基本结构。

栈帧中又包括局部变量表、操作栈、动态连接、方法返回地址等。

(1) 局部变量表:是存放方法参数和局部变量(基本数据类型与引用类型)的区域。相对于类属性变量的准备阶段和初始化阶段来说,局部变量没有准备阶段,必须显示初始化。

(2) 操作栈:是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和提取信息。

(3) 动态连接:每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。

(4) 方法返回地址:方法执行时有两种退出情况:第一,正常退出;第二,异常退出。无论何种退出情况,都将返回至方法当前被调用的位置,相当于弹出当前栈帧。

 

程序计数器

程序计数器:是一块较小的内存空间,可看作当前线程正在执行的字节码的行号指示器

为什么需要程序计数器

 JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响

 

程序计数器作用

1 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理

2 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

 

特点

一块较小的内存空间
线程私有。每条线程都有一个独立的程序计数器。
是唯一一个不会出现OOM的内存区域。
生命周期随着线程的创建而创建,随着线程的结束而死亡

 

Java堆:

  • Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
  • 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
  • Java堆是垃圾收集器管理的主要区域,称为"GC堆"
  • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

 

堆区是JVM中最大一块内存区域,存储着各类生成的对象、数组等,所有通过new创建的对象的内存都在堆中分配,JVM8中把运行时常量池、静态变量也移到堆区进行存储。堆区被细化可以分为年轻代、老年代,其实不分代完全可以,分代的唯一理由就是优化GC性能

如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

  • 新生代。新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中

  • 旧生代。用于存放新生代中经过多次垃圾回收仍然存活的对象

  • 持久带(Permanent Space)实现方法区,主要存放所有已加载的类信息,方法信息,常量池等等

java 内存模型


1 Java采用内存共享的模式来实现线程之间的通信。编译器和处理器可以对程序进行重排序优化处理,但是需要遵守一些规则,不能随意重排序。

原子性:一个操作或者多个操作要么全部执行要么全部不执行;
可见性:当多个线程同时访问一个共享变量时,如果其中某个线程更改了该共享变量,其他线程应该可以立刻看到这个改变;
有序性:程序的执行要按照代码的先后顺序执行;
在并发编程模式中,势必会遇到上面三个概念,JMM对原子性并没有提供确切的解决方案,但是JMM解决了可见性和有序性,至于原子性则需要通过锁或者Synchronized来解决了。

2 happens-before

   happens-before是JMM最核心的概念。对于JAVA程序员来说,理解happens-before是理解JMM的关键

    原则是JMM中非常重要的一个原则,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以解决在并发环境下两个操作之间是否存在冲突的所有问题。JMM规定,两个操作存在happens-before关系并不一定要A操作先于B操作执行,只要A操作的结果对B操作可见即可。

JMM通过happens-before关系向程序员提供跨线程的内存可见性保证:

  1. 程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。

  2. 锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。

  3. volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。

  4. 传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C

  5. 线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。

  6. 线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。

 

3 在程序运行过程中,为了执行的效率,编译器和处理器是可以对程序进行一定的重排序,但是他们必须要满足两个条件:1 执行的结果保持不变,2 存在数据依赖的不能重排序。重排序是引起多线程不安全的一个重要因素。


4 同时顺序一致性是一个比较理想化的参考模型,它为我们提供了强大而又有力的内存可见性保证,他主要有两个特征:1 一个线程中的所有操作必须按照程序的顺序来执行;2 所有线程都只能看到一个单一的操作执行顺序,在顺序一致性模型中,每个操作都必须原则执行且立刻对所有线程可见。

 

JVM内存模型

标签:基本数据   select   sel   contain   根据   attr   并发编程   perm   入栈   

原文地址:https://www.cnblogs.com/shu-java-net/p/13546027.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!