1、JVM简析
对于JVM我们在初学java的时候就已经有所了解,JDK JRE 和JVM这三者的关系。在学习JVM之前我们需要思考一下为什么要学习JVM?
到现阶段我们写代码好像用不到,貌似所有的事情JVM都替我们做好了。在初学的时候我们就已经知道JVM的垃圾回收机制,但我们只闻其名,甚至创建一个String对象JVM做了什么我们都说不清楚,更别说JVM调优等问题了。
1.1、JVM基本概念
- JVM(java virtual machine)也就是java虚拟机
- 我们之前都了解到Java语言有一个特点就是在任何平台都能运行,与平台的无关性。这正是因为有了JVM,Java编译生成的代码只要在JVM上运行即可。不用像别的高级语言需要编译成不同的目标代码。
- JVM在执行目标代码时(字节码),把字节码解释成具体平台上的机器指令。
1.2、JVM内存结构
从这篇博客找到了这张图片,看起来十分清楚
先来整体的了解一下JVM内存结构,由上图可知JVM分为五个区域:虚拟机栈、本地方法栈、方法区、堆、程序计数器。
图中五个区域分为绿黄二色,虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。那么,绿色表示“通行”,橘黄色表示停一停(需等待)。
JVM不同区域的占用内存大小不同,一般情况下堆最大,程序计数器较小。那么最大的区域会放什么?当然就是Java中最多的“对象”了。
1.2.1、堆(Heap)
- 我们从最大的区域来讲,堆的内存最大,是线程共享的,为所有创建的对象和数组分配内存空间。几乎所有的对象实例都在此分配,也就是说凡是new出来的东西都要放在堆当中。我看过很多图,对上的东西都会有地址,通过引用可以获得其地址值。
- 因为堆占用内存空间最大,堆也是Java垃圾回收的主要区域(重点对象)。
1.2.2、方法区(Method Area)
- 方法区与堆联系很紧密,他们也有许多共性:线程共享、内存不连续、可扩展、可垃圾回收。
- 那么相同的地方我们可以把方法区看成堆的逻辑部分。
- 方法区的特点:储存虚拟机加载的类信息、常量、静态变量,也就是存放.class的相关信息,编译后的文件存放在此。
- 方法区也要进行回收,主要是针对于常量池的回收和对类型的卸载。
1.2.3、虚拟机栈(JVM Stacks)
- Java虚拟机栈是线程私有的内存空间,生命周期与线程相同。里面包括方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。这些东西都在一个帧栈里面。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
- 栈中方法的局部变量超出作用域就消失。
1.2.4、本地方法栈(Native Method Stacks)
- 本地方法栈(Native Method Stacks)与虚拟机栈作用相似。区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务,但不是由Java实现的,而是由C实现的。
1.2.5、程序计数器(Program Counter Register)
- 从上图可知,程序计数器是占用内存最小,线程私有。用于记录下一条要运行的指令,每个线程都需要一个程序计数器,Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。
- 程序计数器模块是JVM内存区域唯一不会报outofMemoryError情况的区域。
2、Java内存模型与生命周期
我一开始以为Java内存分析是指Jvm的内存结构,后来才知道这个与多线程编程相关。JVM内存结构、Java内存模型以及Java对象模型这三者,我一开始没有分清楚,而Java内存模型是这三个知识点里面最晦涩难懂的一个。
之前学习了Java多线程,现在也遗忘了,正好先学习Java的内存模型为再次学习多线程打基础。
2.1、Java内存模型
在介绍Java模型之前,我们需要了解计算机内存模型,由于我还没上过计组(也不知道计组教不教这个),我粗略的讲一遍,详细的大家可以看这篇博客:https://blog.csdn.net/hollis_chuang/article/details/80880118。
2.2计算机内存模型
在计算机执行程序的时候,每条指令都是在CPU当中执行的,而在执行的过程中,势必涉及到数据的读写。那么随着技术的发展CPU的执行速度越来越快,而储存数据的内存跟不上了CPU的速度,如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。这时候就出现了高速缓存。
它就是CPU与内存之间的过渡,**那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。**同时CPU缓存有一级缓存,二级缓存,以及三级缓存。
单核CPU只含有一套L1,L2,L3缓存;如果CPU含有多个核心,即多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。
多核CPU出现多线程的问题就出现了,在单核多线程时,CPU的缓存只会被一个线程访问,所以不存在多线程所造成的一系列冲突。
多核CPU,多线程 由于有了多个线程,那么多线程就可以分别在不同的和兴上执行。每个核心都至少有一个一级缓存,会在自己的缓存中保存一份主内存的所需的数据,由于多核可以并行,那么就会存在多个线程之间的数据不同的情况。
2.3、什么是Java内存模型(JMM)
Java内存模型(Java Memory Model ,JMM)一种能够使Java程序在各种平台上对数据的读写保持效果一致,保证并发效果一致性的机制。
我先不讲并发编程要解决的原子性、可见性以及有序性,我们先看多线程是怎么操作数据的。
2.4、线程如何操作数据
线程不能直接对主内存进行操作,主内存是存储所有变量的地方。那么线程如何操作变量,Java模型中每条线程有自己的工作内存。在工作内存中线程将所使用的变量从主内存里面copy一份,进行操作后写到主内存中。不同的线程之间也无法直接访问对方工作内存的变量。
如果Java内存模型只有这两个那势必会导致很多问题,例如线程1正在对一个变量进行操作,还未返回主内存时线程2也对这个变量进行操作,这就是可见性问题。所以我们有了JMM在主内存与工作内存之间,规范了每个线程与主内存如何进行数据同步。
2.5、怎么实现Java内存模型
在Java多线程中提供了很多处理并发的关键词等等,我之前只学了一下Java多线程的基础用的基本都是Synchronized和Lock,接下来介绍volatile。
2.5.1synchronized
相当于在总线上加了一个锁,就是说阻塞了其他 CPU 对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。那么只有等待这段代码完全执行完毕之后,其他 CPU 才能从变量 所在的内存读取变量。由于在锁住总线期间,其他 CPU 无法访问内存,导致效率低下。
2.5.2volatile
当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态。因此,当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。这是缓存一致性原则,与volatile的作用类似。
-
volatile是无锁的,并且只能修饰单个属性
-
什么时候适合用volatile
一个共享变量始终只被各个线程赋值,没有其他操作
作为刷新的触发器,引用刷新之后使修改内容对其他线程可见(如CopyOnRightArrayList底层动态数组通过volatile修饰,保证修改完成后通过引用变化触发volatile刷新,使其他线程可见) -
volatile的作用
可见性保障:修改一个volatile修饰变量之后,会立即将修改同步到主内存,使用一个volatile修饰的变量之前,会立即从主内存中刷新数据。保证读取的数据都是最新的,之前的修改都是可见的。
有序性保障(禁止指令重排序优化):有volatile修饰的变量,赋值后多了一个“内存屏障“( 指令重排序时不能把后面的指令重排序到内存屏障之前的位置) -
volatile的性能
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
2.5.3、并发编程的三个概念
1.原子性
- 原子性:即一个或多个操作 要么全部执行完 要么就都不执行,不会出现执行到一半的情况。
举个最简单的例子,大家想一下,假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
i = 88;
假若一个线程执行到这个语句时,我们暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取 i 的值,那么读取到的就是错误的数据,导致 数据不一致性问题。
-
怎么实现原子性
1. 使用原子性操作: 在 Java 中,对基本数据类型的变量的 **读取**和**赋值**操作是原子性操作,即这些操作是不可被中断的 : 要么执行,要么不执行。
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
其实 只有 语句1 是原子性操作,其他三个语句都不是原子性操作。
语句1 是直接将数值 10 赋值给 x,也就是说线程执行这个语句的会直接将数值 10 写入到工作内存中;
语句2 实际上包含两个操作,它先要去读取 x 的值,再将 x 的值写入工作内存。虽然,读取 x 的值以及 将 x 的值写入工作内存这两个操作都是原子性操作,但是合起来就不是原子性操作了;
同样的,x++ 和 x = x+1 包括3个操作:读取 x 的值,进行加 1 操作,写入新的值。
JVM中定义了一系列的原子操作去实现JVM在计算机内存中的工作方式:
-
- read(读取):从主内存中读取数据
- load(载入):将主内存读取到的数据写入内存中
- use(使用):从工作内存中读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存中的数据写入主存
- write(写入):将store过去的变量赋值给主内存
- lock(锁定):将主内存变量加锁,表示为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
- 要实现更大范围的操作可以通过**synchronized 和 Lock **来实现,保证任一时刻只有一个线程访问该代码块。
2.可见性
- 可见性:指当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 怎么实现:
- 使用volatile修饰共享变量
- 通过 synchronized 和 Lock
3.有序性
- 有序性:在Java内存模型中,指令实际的执行顺序与在文件中所写的不一致,代码指令被编译器和处理器进行了重排序。这不会影响单线程,但是会影响到多线程并发。
- 重排序的意义:那就是为了使处理器内部的运算单元能够尽量的被充分利用。
- 三种重排序:
- 编译器优化( JVM,JIT编辑器等): 编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序
- 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序: 由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
- 如何保证有序性:
- 使用volatile可以保证一定的有序性
- Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。
2.5.4happens-before 原则
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
- volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C ;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
这八条原则摘自《深入理解Java虚拟机》
https://blog.csdn.net/hollis_chuang/article/details/80880118。
https://blog.csdn.net/qq_41170102/article/details/104650162
https://blog.csdn.net/justloveyou_/article/details/53672005