Java内存模型和物理架构详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/klordy_123/article/details/89142433

1. 概述

  我们常说的Java虚拟机具有很好的跨平台性,之所以强调所谓的跨平台性是因为不同的系统底层架构是会有区别的,而Java虚拟机的跨平台性就是它帮助我们把不同系统的底层区别给KO掉了,使得我们通过Java虚拟机编写的代码可以忽略不同平台底层的区别。而这其中关键的一点就是Java虚拟机的Java内存模型,内存模型其实就是JVM内部自己设计了一套规则,这套规则会规定不同线程之间信息同步的规则、访问规则以及修改规则等等,在这些规则的条件下,对于底层不同系统的底层差异就不需要开发人员关心了。
  虽然不同的系统的硬件架构会有区别,但是整体来说是相似的,所以我们在理解Java内存模型设计前,通过了解内存硬件架构会对Java内存模型的理解有很好的帮助。

2. 硬件内存架构

  对于硬件内存架构的理解首先涉及到三个重要的概念:CPU寄存器、CPU缓存以及主存,CPU在访问这三者的速度而言差别是很大的,关系是:CPU寄存器>CPU缓存>主存,最开始的时候是没有CPU缓存一说的,之所以出现它就是因为CPU寄存器和主存之间访问速度差别太大,导致计算机性能有较大缺陷,所以后来在中间加入了CPU缓存来适当解决这个问题,这一块三者之间的关系和区别不再赘述,可以参考:https://blog.csdn.net/hellojoy/article/details/54744231
了解了以上三个概念后,我们来看看硬件内存架构图:
硬件内存架构图
  现在的计算机多数都是多个CPU的,一个CPU可能是多核,如图所示每个CPU内部有自己的一个CPU寄存器,多个CPU共享主存,然后由于CPU访问主存速度相比CPU寄存器要慢很多,所以每个CPU和主存之间又有一个中间的过度区域为CPU缓存。
  以我们所谓的多线程应用来说,要知道的一点是线程和进程的区别,简单理解就是进程就是一个应用,系统在进行资源分配的时候是给进程进行分配的,而线程则是依托于进程的更小的单位,一个进程内部可以有多个线程,多线程之间会共享系统分配给该进程的主存空间,线程是CPU进行调度的基本单位,CPU在分配时间片时是分配到线程级别的。所以上图的硬件内存架构中,可以清楚的看到多个CPU之间是共享主存的,这就好比我们说的多个线程之间是共享系统分配给进程的主存空间类似。
  以上就是关于硬件内存架构的介绍,那么接下来我们再来看Java内存模型的介绍。

3. Java内存模型

  首先,看到这里的人都应该是知道JVM内存划分主要可以简单的分为线程栈和堆(其它的方法区、程序计数器等忽略),对于堆和栈的内存使用来说,其实模式是和刚才介绍的硬件内存架构是基本一致的,如下:
JVM内存划分
  可以看到图中意思就是,一个JVM内部的内存布局,每个线程都会有自己的一个线程栈,然后我们经常在多线程的时候会需要进行线程间通信,那么多个线程栈之间建立联系的方式就是通过所有线程栈所共享的堆内存。
  这里我们先来简单介绍一下线程栈和堆中所存储的内容,对于堆来说,应用中所有产生的对象都会分配在堆中(不包括基本类型的局部变量),然后对于线程栈来说,它的结构就是一个栈,出栈入栈的就是一个个的栈帧,一个栈帧对应着一个方法的调用,我们在Java编码的时候,其实各种逻辑啥的都是在方法中实现的,包括主程序入口也是在main方法中开始的,所以一个Java应用整个生命周期就是不断的各种方法的调用,然后对于每个方法的调用都会封装为一个个的栈帧,funcA -> funcB -> funcD这种调用链来说就会在对应这个线程栈中插入三个栈帧,每个栈帧中会记录方法调用的一些局部变量信息、上个调用方法的返回地址等信息,在funcD这个栈帧中处理逻辑完毕后,会把返回值传递给记录的返回地址处,然后回到funcB对应的栈帧中,如此过程就形成的程序的运行过程。每个栈帧的大小在程序编译的时候就已经是确定了的,而且栈的最大所需深度也是确定的。
  另外对于各种局部变量而言,某些局部变量可能不是简单的基本类型,而是某个类,那么这个时候在栈帧中存储的只是对于这个对应的引用,而对象的具体内容是存储在堆中的,在需要使用到对象的时候通过引用去堆中获取对象的信息,我们先来看一个图:
堆栈信息
  上图表示有两个线程栈,其实它内部的代码是完全一样的,例如可能是多个线程执行某个一样的逻辑,然后这两个线程中可能就会涉及到共享某个变量的情况,这个时候就会出现我们常说的所谓线程安全问题了!具体解释这一点,我们首先要知道如果多个线程共享某个变量,那么它们是如何访问这个存在于堆中的共享变量的呢?线程在需要获取堆中信息的时候,不是我们所谓的简单拿过来用完再还回去,而是会把这个堆中的数据拷贝一份作为自己这个线程的私有变量形式来使用,这个时候相当于堆中会有一份原始数据,然后线程工作的私有内存中也会有一份拷贝过来的数据,然后在使用完毕后,如果数据有变化在把变化的数据更新到堆中。此时如果没有锁的话,就会出现两个线程同一时间都把堆中某个共享变量拷贝到自家私有内存中进行处理完毕,然后假设都做了变化,那么再把变化更新到堆中的时候其中一个线程的数据会被另外一个线程的数据所覆盖掉,这样就会出现所谓的线程不安全。所以为了解决上面的问题,Java内存模型就会规范关于变量操作的一些规则,在这个规则之下可以避免这种情况的出现。
  这里还有一个问题就是,Java中的堆栈是对应分配在硬件内存架构中的哪块区域呢?对于第二个问题,我们在开头说的CPU寄存器、CPU缓存以及主存介绍的帖子中可以看到一点是,CPU寄存器的大小一般在512KB左右,CPU缓存大小一般在12MB这种量级,然后我们在考虑我们平常Java虚拟机堆栈内存分配都很可能分配在GB级别,所以很显然堆栈应该是主要分配在主存中的,当然网上有说有部分堆栈信息可能会出现在CPU缓存或是寄存器中,这个个人不是很了解,有人知道这部分内容的可以介绍一下。
  知道以上内容后,我们再来简单介绍一下volatilesynchronize关键字在这里面的实现原理,对于volatile而言,如果是多个线程共享的变量,在被volatile修饰后,那么对于这个变量的所有读取和修改操作都会直接从主存中进行,而不是和之前说的那样把这个对象拷贝一份到线程本地作为私有变量操作。这样就可以使得如果线程A对共享变量进行了修改,然后线程B可以立刻看到主存中的最新值,不过这种只是保证了如果值更新后可以马上能让其它线程看到,但是这也不是线程安全的,因为可能线程A从主存读取变量值还没来得及修改更新回主存,然后线程B就来获取了主存中的旧值然后也进行修改,这样最终还是会出现某一个线程的操作被覆盖。所以volatile只能保证可见性但是还是线程不安全,为了解决这个问题就引入了synchronized关键字,这个关键字的作用就是保住同一个时刻只能有一个线程访问到synchronized所修饰的变量或是代码区域,并且变量的所有读取更新操作都是直接操作主存的,处理完毕后所有更新同步到主存完毕后才会开发这个区域让线程B来访问,此时就不会出现线程不安全的问题了。

展开阅读全文

没有更多推荐了,返回首页