深入理解JVM(二)JVM内存模型
一、前言
上文讲过了虚拟机的内存划分,即,我们将内存分为线程共享和线程私有。
线程共享的即java堆,和方法区。java堆大家可能都不会陌生;而方法区中包含了常量池,他也被称为永久代。通常方法区也会被叫做非堆,但是在逻辑上,他却是java堆的一部分,而且有些虚拟机会将方法区直接与java堆合并。
线程私有的就是虚拟机栈了,而虚拟机栈,本地方法栈,以及程序计数器。这里我们就不展开讨论了。
上面我就简单的回顾了虚拟机的内存划分部分,下面开始正文。
二、java内存模型简述
1、主内存
java内存模型规定了,所有的变量都必须存储在主内存当中。
2、工作内存
每天线程私有的内存,即工作内存。
工作内存中保存了该线程所使用的变量的主内存的副本的拷贝。线程对变量所做的操作,都必须在工作内存中进行。
不同个的线程,无法访问对方的工作内存变量,只能通过主内存,来达到线程、工作内存、主内存三者之间的信息交互。
简图如下:
主内存、工作内存,与我上一篇博客中讲述的java内存区域中的堆、栈、方法区等,并不是同一个层次的内存划分。
不同,为了方便记忆,我们可以这么理解:
主内存对应的是java堆中的实例数据部分,工作内存对应的是java虚拟机栈中的部分区域。
从计算机的组织原理来说,我们也可以这么来理解,主内存对应的是物理硬件的内存,所以如果主内存与进程进行数据交互,它将是非常耗时的。
工作内存优先存储在寄存器和高速缓存中,因为程序在运行一般访问的是工作内存。
(所以我在上篇博客的开头就讲了,抛开操作系统和组织原理来讲虚拟机,就是在耍流氓 =_=)
三、关于原子性的二三事
1、从一段代码开始
No BB, show code
private static volatile int i = 0;
public static void add(){
i++;
}
public static void main(String [] args){
for(int c=0; c<20; c++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int k = 0; k<10000; k++){
add();
}
}
});
thread.start();
}
while (Thread.activeCount()>1){
Thread.yield();
}
System.out.println(i);
}
如果你看过上面的代码,那可以继续阅读,如果没有见过上面的代码,这里建议思考下,最后输出的值是多少?
显而易见,结果并不是200000。(如果最后的结果就是200000,那么我举这个例子干嘛 =。=)
2、虚拟机内部的原子操作
无论是长辈,还是其他人的建议,都提过,带着问题阅读的效率会比漫无目的阅读,效果好很多,所以上面我提出了问题,下文自然是为了解决问题而展开的额讨论和说明。这里,先从java虚拟机内存的操作开始讲起。
lock:作用于主内存,将主内存的某变量标志为一条线程独占。
unlock:作用于主内存,将主内存中的变量,从锁定状态解放出来,解放出来的变量,才可以重新被其他线程占用。
read:作用于主内存,将主内存中的变量,从主内存传输到工作内存中。
load:作用于工作内存,将read到的值,放到工作内存的副本当中。
use:作用于工作内存,将工作内存中的一个变量,传递给执行引擎。当虚拟机执行的字节码指令,运用到此值时,使用此操作。
assign:作用于工作内存,将从执行引擎接受到的值,赋值给工作内存的变量。每当执行字节码的赋值语句时,会使用此操作。
store:作用于工作内存的变量,将工作内存中的变量,传输到主内存中。
write:作用于主内存,将store中从工作内存获取到的变量,放到主内存的变量当中。
3、原子操作的划分
原子操作分为两部分,一般,通过read、load、use、write等读写操作,就可以保证数据的原子性。
但是有时候我们需要整块的业务代码,都具有原子性时,就需要使用lock与unlock。
4、volatile说明
细心的同学可能已经发现了,我上面的代码中。遍历时被volatile声明。
那么volatile的作用是什么呢?
一般来说,volatile变量对所有的线程,都是理解可见的。对于volatile变量所有的写操作,都能理解反应到其他线程中。
换言之,volatile在所有线程中都是一致的,所以,所有基于volatile变量的运算在并发下都是安全的。
其实不然,volatile变量,并不能保证并发安全。
(1)执行结果对比
变量类型 | 执行结果1 | 执行结果2 | 执行结果3 | 执行结果4 | 执行结果5 | 平均值(去掉极值) |
---|---|---|---|---|---|---|
volatile | 186632 | 196403 | 193658 | 197305 | 186825 | 192295 |
一般变量 | 178387 | 179369 | 189835 | 174015 | 199458 | 182530 |
我记录了五次代码的执行结果。如上表格所示。都不是我们的目标值200000。那是不是说明volatile声明的变量和不进行声明,是完全一致的呢?
非也,我在去掉了volatile声明后,执行得到的结果,如上表格展示。
最后得出的结论是,加了volatile声明,结果更加趋近目标值。造成这一现象的原因是什么呢?
(2) 从字节码开始说明
查看字节码的方式有一般有两种。
一是找到生产的class文件,执行 javap指令,查看编译的代码。
二是,如果你用的是idea编辑器(idea天下第一),你可以在选中要查看的java类后,点击view菜单 点击 Show Bytecode。
这两种方式,我一般选择方式二,方式二方便,且查看的代码格式符合我的阅读习惯。
public static void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
LineNumberTable:
line 12: 0
line 13: 8
volatile声明对象的字节码指令执行流程说明
volatile变量声明的对象,的的确确是,当他在主内存中的值发生变化,会立即反应到工作内存中。
这里就有一个节点,也就是我们字节码中的
getstatic
getstatic指令,此指令,是获取了当前最新的实时的变量值。后续的对此值进行+1操作,然后返回。但是可能存在一个情况,就是在执行+1操作或者返回操作时,其他线程对这个值进行了处理,导致此线程返回的值并不是正确值了。
可能还是不太理解,我们模拟一下场景。
- 场景①
时刻1:线程A获取了此值1。(最快)
时刻2:线程B获取了此值1。(次快)
时刻3:线程C获取了此值1.(最慢) - 场景②
时刻4:线程A处理完毕了值,且write了值到主内存,执行完毕后主内存的值为2.
时刻5:线程B,在线程A修改完主内存值后,才开始执行getstatic指令,最后他执行的是2+1,执行完毕后主内存值为3
时刻6:线程C,在执行getstatic方法时,线程A已经写完数据到了主内存,而线程B还在进行+1操作。所以此时主内存值为2。他再对2进行+1操作,执行完毕后,将3写入主内存。
所以最后主内存的值为3,并不是我们的目标值4。这也是我们的代码执行结果了,小于200000的原因。
不加volatile声明对象的字节码流程说明
不加volatile声明,可能在进入线程后,未进行getstatic指令前,变量值发生了改变,而线程不知道。
所以,这也就是加了
四、线程安全的正确姿势
讲到这里,我想大家应该对上方的代码执行结果没有什么疑虑了。
但是问题又来了,如何确保能正确的得到目标值呢。
1、万能的synchronized
相比大家看到此关键字,就已经知道了我下面要讲什么了。
public synchronized static void add(){
i++;
}
对add方法,加了synchronized关键字进行修饰之后,最后得到的目标结果,就是我们的目标值20000了。
当然,越是万能,往往代表越是无能。
此方法的性能会比使用自己手动的进行lock以及unlock,性能要差很多。
特别是在1.5的jdk版本,性能差异非常大。不过在后续的jdk版本中,逐渐对synchronized在进行优化。而且官方也推荐这种方式,毕竟,他较之ReentrantLock要优雅、coooooool很多。
2、高性能的ReentrantLock
private static int i = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void add(){
lock.lock();
try{
i++;
}finally {
lock.unlock();
}
}
即使是i++,我们也要进行try,这是为了养成良好的语义习惯 =_=
每一次加锁,必然要进行一次解锁。不然....嘿嘿嘿嘿
需要说明的是,ReentrantLock(重入锁)比之synchronized,多了其他的高级功能,等待可中断、实现公平锁、所可以绑定多个条件。这里就不进行展开讨论。
3、狭隘的AtomicInteger
private static AtomicInteger i =new AtomicInteger(0);
public static void add(){
i.addAndGet(1);
}
Atomic对象有很多,如AtomicBoolean、AtomicLong等。
他保证了数据操作的原子性,实现原理是通过CAS原理。
何为CAS?即比较和交换:
获取主内存值(A),将获取到的值(A)与新的值(B)放入参数。在此获取其值,如果,获取到的值与传输的值A一致,就修改主内存值为新的值B。
这也就是CAS
当然在Atimic的实现中,还是用了Unsafe类,他可以直接操作物理内存!!!!
这里我们不对他详细的展开论述。
五、总结
内存模型中,分为工作内存与主内存。
这么讲其实没意义,我换个说法,为什么要区分工作内存和主内存??
线程是程序运行的基础,而线程需要与计算机进行数据交换,而由于计算机的组成,进行数据交换会,有的内存区域传输快,有的传输慢。而且也为了保证数据的安全性,我们区分出了主内存(可以狭义的理解为物理内存)与工作内存(寄存器即高速缓存,当量大时,也会存储到物理内存中)
在重温JVM时,我多次的是思考了为什么?也就是为什么要这么设计,这么设计有什么好处,收益颇多。
六、参考
《深入理解Java虚拟机》
深入理解JVM(二)JVM内存模型的更多相关文章
- JVM 系列(二)内存模型
02 JVM 系列(二)内存模型 一.JVM 内存区域 JVM 会将 Java 进程所管理的内存划分为若干不同的数据区域.这些区域有各自的用途.创建/销毁时间: 一. 线程私有区域 线程私有数据区域生 ...
- (转载)JVM中的内存模型与垃圾回收
转载自微信公众号:Java高级架构(Java-jiagou)-----看完这篇文章,我奶奶都知道JVM中的内存模型与垃圾回收了! 六.内存模型 6.1 内存模型与运行时数据区 Java虚拟机在执行J ...
- JVM学习笔记——内存模型篇
JVM学习笔记--内存模型篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的内存模型部分 我们会分为以下几部分进行介绍: 内存模型 乐观锁与悲观锁 synchronized优化 内 ...
- 深入理解JVM(二)——内存模型、可见性、指令重排序
上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...
- 轻松学JVM(二)——内存模型、可见性、指令重排序
上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...
- 深入理解JVM(6)——Java内存模型和线程
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(“即Ja ...
- 深入理解JAVA虚拟机(内存模型+GC算法+JVM调优)
目录 1.Java虚拟机内存模型 1.1 程序计数器 1.2 Java虚拟机栈 局部变量 1.3 本地方法栈 1.4 Java堆 1.5 方法区(永久区.元空间) 附图 2.JVM内存分配参数 2.1 ...
- 深入理解JVM(二)Java内存区域
2.1 C.C++内存管理是由开发人员管理,而Java则交给了JVM进行自动管理 2.2 JVM运行时数据区:方法区.堆(运行时线程共享),虚拟机栈.本地方法栈.程序计数器(运行时线程隔离,私有) 1 ...
- 理解JVM之java内存模型
java虚拟机规范中试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统内存访问差异,以实现让java程序在各种平台都能打到一致的内存访问效果.所以java内存模型的主要目标是定义程序中各 ...
- 【JVM】JVM系列之内存模型(六)
一.前言 经过前面的学习,我们终于进入了虚拟机最后一部分的学习,内存模型.理解内存模型对我们理解虚拟机.正确使用多线程编程提供很大帮助.下面开始正式学习. 二.Java并发基础 在并发编程中存在两个关 ...
随机推荐
- win10 出现 No AMD graphics driver is installed or the AMD driver is not functioning properly .....
原因:win10的自动更新的功能没有关闭,更新有时候会出现显卡驱动更新不及时出现的问题. 解决方法一:使用 驱动人生(或者等等....) 进行升级驱动. 解决方法二:手动升级. 1.打开设备管理器 2 ...
- python瞎练
需求:有不规则列表 singlelist3 = [ '总计', '每吨人工:', '总人工', 1748.07, '金额'],如果当前元素为字符串且该元素的下一个相邻位置仍为字符串,那么请在该元素后面 ...
- [2019HDU多校第一场][HDU 6588][K. Function]
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6588 题目大意:求\(\sum_{i=1}^{n}gcd(\left \lfloor \sqrt[3] ...
- [Javascript] How to deal with floating number
What's your expect of the output?: console.log(0.1 + 0.2 === 0.3); The answer is 'false'. Because: 0 ...
- 【DES加密解密】 C#&JAVA通用
DES加密解密 C# Code /// <summary> /// DES加密解密帮助类 /// </summary> public static class DESHelpe ...
- Spring Security 自定义 登陆 权限验证
转载于:https://www.jianshu.com/p/6b8fb59b614b 项目简介 基于Spring Cloud 的项目,Spring Cloud是在Spring Boot上搭建的所以按照 ...
- laravel事件监听器
在EventServiceProvide文件里注册事件和监听 protected $listen = [ 'App\Events\SendPhoneCodeEvent' => [ 'App\Li ...
- WebUI自动化之Java语言提高
单独写一个函数和把函数写在类中的区别: 单独写一个函数,函数只能完成一个功能,团队开发.让第三方使用时比较麻烦: 项目管理和构建自动化工具Maven:
- [Luogu] 排序机械臂
https://www.luogu.org/problemnew/solution/P3165 预处理 我们会发现一个问题:高度是无序的,而splay中要求有序,否则kth不能正确求解.不需要求高度, ...
- Mongodb内存管理和使用情况查询
overview MongoDB使用的是内存映射存储引擎,即Memory Mapped Storage Engine,简称MMAP.MMAP可以把磁盘文件的一部分或全部内容直接映射到内存,这样文件中的 ...