平时我们比较容易搞混的JVM内存结构、Java内存模型之间的概念,以为JVM内存结构、Java内存模型是一样的,其实这两者有着很大的区别。
JVM内存结构
Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。有些区域是线程共享的,有些区域是线程独占的,因线程启动而建立,因线程结束而销毁。JVM运行时内存区域结构如下:
1.1 程序计数器
- 线程私有
- 计数器记录的是正在执行的虚拟机字节码指令的地址,如果是Native方法,计数器值为空(Undefined)
1.2 Java虚拟机栈
- 线程私有
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息等,每个方法从调用直至执行完成,就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程。
1.3 本地方法栈
- 本地方法栈与虚拟机栈的作用差不多,两者之间的区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
1.4 Java堆
- 线程共享
- 存放对象实例和数组,由于逃逸分析技术的成熟,对象实例和数组不一定分配到堆上了,也会进行栈上分配,便于回收
1.5 方法区
- 线程共享
- 存储已被虚拟机加载的类信息(类名、访问修饰符、字段描述、方法描述等)、常量、静态变量、JIT编译后的代码等数据
1.6 运行时常量池
- 线程共享,属于方法区的一部分
- 存储编译期生成的字面量(常量)和符号引用
符号引用与直接引用的区别
符号引用就是一个包含足够信息的字符串,以供实际使用时找到相应位置。比如某个方法的符号引用,运行一次之后根据这个字符串的内容找到该类方法表中的方法位置,运行一次之后,符号引用就会被直接引用替换。
直接引用就是偏移量,虚拟机能够通过该偏移量直接找到该类的内存区域中该方法字节码的起始位置。
1.7 直接内存(堆外内存)
- 不是虚拟机运行时数据区的一部分
- 通过堆内内存中的DirectByteBuffer对象对直接内存中的对象进行操作
Java内存模型
由JVM内存结构可以知道,Java堆和方法区是线程共享的数据区域,多个线程可以读写在Java堆和方法区中的数据,也就是共享资源,因此可以认为Java的多个线程是通过共享内存进行通信的。
Java内存模型:Java Memory Model(JMM),只是一个抽象的概念,并不是具体存在的内存结构。JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
在多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。
Java的多线程之间是通过共享内存进行通信的,由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。
导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,但是程序的性能可就堪忧了。
合理的方案应该是按需禁用缓存以及编译优化。所谓“按需禁用”其实就是指按照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。
Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括volatile、synchronize和final三个关键字,以及Happens-Before规则。
Happens-Before规则
- 1.程序的顺序性规则
这条规则是指在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作。
- 2. volatile 变量规则
指对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。
- 3.传递性
这条规则是指如果A Happens-Before B,且B Happens-Before C,那么A Happens-Befores C。
- 4.管程中锁的规则
这条规则是指对一个锁的解锁Happens-Before于后续对这个锁的加锁。管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现。管程中的锁在Java里是隐式实现的。
- 5.线程start()规则
它是指主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B前的操作。
换句话说就是,如果线程A调用线程B的start()方法(即在线程A中启动线程B),那么该start()操作Happens-Before于线程B中的任意操作。
- 6.线程join()规则
这条是关于线程等待的。它是指主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法实现),当子线程B完成后(主线程A中join()方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的就是对共享变量的操作。
换句话说就是,如果在线程A中,调用线程B的join()方法并成功返回,那么线程B中的任意操作Happens-Before 于该join()操作的返回。
- 7. 线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 8.对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
final关键字
final修饰变量时,初衷是告诉编译器:这个变量生而不变,可以随便优化。
只要我们提供正确构造函数没有“逸出”,就不会出问题了。
Java对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
HotSpot虚拟机中,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个Class,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个实例对象,这个对象中包含了对象头以及实例数据,并且对象头中元数据指针指向方法区中的Class,表明这个对象是哪个类的实例。
总结
JVM内存结构是Java程序在Java虚拟机中运行时的数据区域结构,每个区域都有每个区域的特性和功能;Java内存模型是为了解决并发时多线程访问共享内存中的数据时可能出现的线程不安全问题,而定义的一些规范;Java对象模型就是Java对象在虚拟机中的表现形式是怎么样的。