Java Memory Model抽象
一.JVM运行时的数据区
我们写的JAVA代码最终都会编译成字节码,然后在JVM虚拟机上运行。在运行字节码的时候,JVM需要知道要运行的字节码是什么,也就是说JVM本身需要存储字节码,那么字节码存储在JVM的什么地方呢?同时在运行字节码的时候,会产生一些运行时数据,这这些数据都存储在什么地方呢?我们new一个Object的时候,这时候我们就获得了一个对象的引用,那么这个引用指向的内存空间在哪里呢?在C/C++中我们经常使用malloc去给分派一块内存空间,然后返回一个指针,当然在JAVA中内存空间的分派也是少不了的,只不过JVM帮你分派了,你就不需要手动去分派了。
下面我们先看一张图,这张图描述了JVM运行时的数据区
上面这张图很清晰的列出来了JVM运行时数据区的划分。
方法区
又称perm区(永久代),这里面主要存储一些类本身的信息,比如字节码,字面意义的常量等等,我们经常动态修改类的字节码,或者动态加载一个类,其实都是修改perm区中的内容,或者占用perm区中的空间。虚拟机栈
JVM在执行字节码的时候,都是基于栈进行,读取栈顶的数据,做相关计算,然后入栈,同时还有局部变量表来配合。本地方方法栈
主要指native方法的调用,使用jstack -m可以打印出native方法的调用堆栈。堆
我们经常new一个Object,这个Object所占的内存空间就是从堆上分配来的。堆又被划分为新生代和老年代,这是为了支持GC的分代回收,发生在新生代是YGC,之所以有YGC是因为大数据JAVA对象都是朝生夕死的,YGC的成本低,因此执行一次YGC就能回收到这些朝生夕死对象所占的内存了。发生在老年代是FGC,要是对象在新生代存活到一定时间后,就会被转移到老年代,老年代中对象所占的内存空间要是想被回收,就只能有FGC来回收了。程序计数器
这个我们经常称为PC,我们需要执行的字节码都会存储在一个数组中,PC指向一个数组下表,表示当前需要执行那个字节码,注意数组中的每个元素都是一个字节码。
二.JAVA 内存模型的抽象
JMM是为了解决什么问题呢?
1.线程之间的通信
关于线程之间的通信就是线程A所产生的数据,能够被线程B感知到。2.线程之间的同步
两个线程之间通常需要同步,当线程A处于某一特殊状态的时候,线程B才能够执行。
下面我们先来看看JMM的抽象示意图,通过这个示意图我们来描述JMM是如何解决上面两个问题的。
每个线程我们都抽象出一个线程本地内存来,其实这个线程本地内存物理上压根就不存在,这只是一个逻辑上的概念,那么这个线程本地内存到底是什么呢?其实就是缓存,写缓冲区,寄存器以及其他的硬件和编译器优化等。线程A和线程B的共享变量既会在主内存中中保持一份,也会在线程本地内存中保持一份,线程直接从本地内存中读写,JMM控制本地内存中的东西写到主内存。当然JMM也可以控制线程直接读写主内存,主内存中的东西线程A和线程B共享。既然JMM可以控制线程直接读取主内存,这样不管任何情况下主内存中的东西对于其他线程都是可见的,那么为什么不让JMM直接控制线程去读写主内存呢?原因很简单就是为了快,还记得硬件中的二级缓存不,就是这个道理。并不是所有的共享变量都需要在某一时刻被另外一个线程感知到的。
那么上述模型是怎么解决一开始我们提出来的两个问题的呢?
线程之间的通信
关于线程之间的通信,看上图就很明确了,线程A和线程B通过主内存来通信,关键是线程B能否看到线程A对共享变量的真实修改吗?这个是我们写代码的时候需要注意的,比如你可以使用volatile关键字,保证每次线程的写都刷到主内存中,线程的读都从主内存中读取。线程之间的同步
线程之间的同步也是基于主内存共享来实现的,在主内存中找一个共享变量X,在这个共享变量X上加上一把锁,当线程A要想执行某个同步的方法或者同步代码块的时候,需要先获取到共享变量X上的所,这时候其他线程只能等线程A把所释放后才能进执行相应的代码。线程A执行完相应的代码块后,就释放共享变量X上的锁,保证其他线程可以进入同步方法或者同步代码块。关于同步我们可以使用juc包中提供的一些类,也可以使用synchronized关键字,关于synchronized关键字是最为常见的一种同步方式,这里顺带总结一下这个关键字的作用:1.互斥,有且只能有一个线程获得锁
2.获取到锁,所有共享变量从主内存中reload
3.释放锁后,所有本地内存的东西flush到主内存
4.静态方法上synchronized锁对象是java.lang.Class类实例,非静态方法上synchronized锁对象是当前类的对象。