JVM内存结构和java内存模型(JMM)
JVM内存结构和java内存模型(JMM)网络上很多文章,有很多的误导性,但这俩是不一样的东西.
JVM内存结构指的是JVM的运行时的内存分区,如堆,栈,方法区等
JMM(Java内存模型)是java虚拟机的一种规范,它规定了java虚拟机与计算机内存是如何协同工作的,定义程序中各个共享变量的访问规则,在多线程的场景下实现数据一致性.
先讲JVM内存结构:
方法区(常量池位于方法区),堆,虚拟机栈,本地方法栈,程序计数器
其中方法区和堆 是所有线程共享
虚拟机栈,本地方法栈,程序计数器为线程私有
JVM内存结构各区域简介:
程序计数器:
存储的是线程下一步将执行的指令的地方,唯一一个不会OOM的地方,为线程所私有(作用就是记录当前线程执行的位置,当线程重新获取CPU执行权的时候,直接从记录位置执行,程序的分支,循环,跳转也是看程序计数器完成的)
虚拟机栈:
线程私有,存放局部变量表,操作数据,方法出口等,对象存储在堆里,栈里面只是对象的引用,
线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverFlowError的异常(递归调用)
如果在扩展的时候超过了内存大小的限制,就会出现OutOfMemoryError的异常(OOM)。
本地方法栈:
和Java虚拟机栈类似,不过是为JVM用到的Native方法服务。
堆:
线程共享,用于存储对象实例,也会OOM,JVM中最大的一块,内存回收的主要发生地,JDK7时通常分为新生代和老年代
方法区:
与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
一些琐碎的知识点:
JVM版本1.7于1.8内存区别:
1.8和1.7最大的区别,元数据区取代了永久代,元数据区不在虚拟机内部,而是直接使用的本地内存,1.8之后,元数据区的限制就是电脑内存限制了
永久代和元空间的区别
永久代:在运行时数据区域开辟空间实现方法区
元空间:在本地内存区域开辟空间实现方法区
方法区,永久代,元空间的区别: 方法区:是一种定义,规定了里面放那些东西,而永久代和元空间是方法区的不同实现方式
(注意: 移除永久代的原因,永久代中的元数据信息在每次FullGc时可能被收集,为永久代分配多少空间很难确定,超出指定空间容易造成内存泄漏)
这里基本摘抄至:https://blog.csdn.net/u011635492/article/details/81046174
Class文件常量池: Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;声明为final的常量值指的是成员变量,不包含本地变量,本地变量是属于方法的。这些都在常量池的 UTF-8 表中(逻辑上的划分);符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用;
运行时常量池: 运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。
字符串常量池: 字符串常量池则存在于方法区 关于字符串常量池可以参考: https://www.cnblogs.com/fanBlog/p/11311533.html
字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池。
运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。
其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。
String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。
JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,
都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,
超出此范围仍然会去创建新的对象。其中:
在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;
在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;
jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。
当new一个对象时JVM都发生了什么?(jdk1.8的环境下)
1, 判断类是否已经加载 没有加载了执行类加载过程,类加载过程,是将class信息(类的元数据)加载进方法区,然后为Class对象(不是实例)分配内存放在堆里,
Class对象(不是实例)是存放在堆区的,所有实例共享一个Class对象,类的静态(static)变量存于Class对象中
2, new出来一个对象,是在堆中(并不是所有的对象创建都是在堆里,有个叫逃逸分析的: https://blog.csdn.net/youanyyou/article/details/91971717)
3, 对象的引用在栈里
下面讲Java内存模型(JMM):
java语言的跨平台是由java虚拟机实现的,JMM是对java虚拟机的一种规范,是为了屏蔽各种硬件和操作系统的访问差异,保证java程序在各个平台下都能得到一致效果的一种截至和规范;
保证并发编程中线程安全的,JMM中定义了程序中各个变量的访问规则,从而在并发编程中实现数据一致性
为了保证共享内存的正确性(原子性,缓存一致性(可见性),有序性),内存模型(JMM)就定义了共享内存系统中多线程程序读写操作的行为规范,
通过这样的规范,定义了程序中各个变量的访问规则,从而在并发编程中实现数据一致性,它跟CPU有关,跟缓存有关,跟并发有关,跟编译器有关,
他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。针对这些问题,不同平台的都有各自的解决方案,
java语言,为了屏蔽掉底层差异,定义了一套自己的内存模型规范,即JMM
JAVA内存模型的存在主要是为了解决原子性,可见性(缓存一致性),有序性:
原子性: 指一个操作是按原子的方式执行的。要么该操作不被执行;要么以原子方式执行,即执行过程中不会被其它线程中断。线程时cpu调度的基本单位,cpu有时间片概念,会根据不同的算法进行调度,所以再多线程下就会发生原子性问题,例如一个线程要完成一个操作,只进行到一半,时间片耗完了,只能等待重新被调度,这种情况下,这个操作就不是原子性的,即存在原子性问题
缓存一致性(可见性): 在多核cpu多线程的场景中,每个核至少有一个L1缓存,在多线程场景下,多核可以同时写各自的缓存,那么就有可能出现,同一个数据的缓存内容在各个缓存中不一样的情况,这就会出现,缓存一致性问题
有序性: 由于处理器优化和指令重排等,CPU还可能对输入的代码进行重新排序, 这就有可能出现有序性的问题
JMM定义的线程和主内存之间的抽象关系:
1,所有的共享变量都在主内存中
2,每条线程都有自己的工作内存(抽象概念:涵盖了缓存、写缓冲区、寄存器等),工作内存保存了该线程用到共享变量的拷贝副本
3,线程对变量的所有操作都在工作内存中完成,不能直接读取主内存,不同线程直接也无法直接访问对方的工作内存中的变量
JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步
JAVA内存模型(JMM)定义了 8种操作来完成主内存和工作内存的交互操作:
1,lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
2,unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
3,read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
4,load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
5,use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
6,assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
7,store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
8,write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
定义了8种操作同时也规定了一些规则,
1,如果要吧一个变量从主内存中复制到工作内存中,就要按顺序的执行read(读取)和load(载入),如果要把工作内存中的变量同步到主内存中,就要顺序的执行store(存储)和write(写入)
但java内存模型只要求上述操作是顺序执行的,但是不是必须连续的执行,且read(读取)和load(载入),store(存储)和write(写入)必须成对出现,不允许单独出现.
2,不允许线程丢弃它最近的assign(赋值)的操作,即变量在工作内存中改变了必须同步到主内存中,且没有赋值操作不允许变量同步到主内存中
3,一个新的变量只能在主内存中诞生,不允许工作内存中直接使用一个未被初始化的变量,即对一个变量use(使用)和存储(store)之前必须先经过assign(赋值)和load(载入)
4,一个变量同一时刻只能被一条线程进行lock 且可以多次lock,只有进行同样次数的unlock变量才能解锁,lock和unlock必须成对出现,且不允许unlock其他线程ock(锁定)的变量
5,执行lock,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新load(载入)或者assign(赋值)操作初始化变量的值
6,执行unlock之前,必须先把此变量同步到主内存中必须执行store(存储)和write(写入)
JMM是如何解决问题的:参见:https://zhuanlan.zhihu.com/p/29881777
原子性:在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。
可见性: Java中的synchronized,volatile和final两个关键字也可以实现可见性。
有序性: volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。