深入理解JVM 系列(一)JVM运行机制 JVM 内存模型(v
2017-09-05 本文已影响126人
Gxgeek
为了 接下去 更好理解 JAVA 并发,多线程 JUC 包的原理 特此写下前置学习文章 深入学习 java 虚拟机
本文目录
- JVM启动流程
- JVM基本结构
- 内存模型
- 编译和解释运行的概念
一、java 程序 启动流程
启动流程java 命令开始
寻找 配置文件 定位需要的 .dll
.ddl 初始化 JVM 虚拟机
获得 native 接口
找到main 方法运行
二、JVM结构(运行时数据区)
JVM结构(运行时数据区)- 二.一、线程私有的区域
1.程序计数器(PC寄存器)
-
记录正在执行的字节码地址,可辨别当前字节码解析到了什么位置,引导字节码解析顺序,并控制程序的流程。(当前程序执行到哪然后下一步该执行什么操作。)
- 执行java 方法的时候世纪路字节码的地址,执行Native方法这个计数器为空(Undefined)
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
-
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。
2.栈(VM Stack)
- 栈由一系列帧组成(因此Java栈也叫做帧栈)
- 线程私有------->方法执行时候的内存模型
- 每个方法执行都会创建一个栈帧 用于存储局部变量表(基本数据类型和对象引用),操作数栈,动态链表,方法出口等信息,值得一提的是long,double长度为64为会占据两个局部变量空间其余为一个。
局部变量表所需内存实在编译器完成分配的,方法在帧中所需的分配多大的局部变量空间是完全确定的,方法运行不会改变局部变量表大小。
- 每个方法执行都会创建一个栈帧 用于存储局部变量表(基本数据类型和对象引用),操作数栈,动态链表,方法出口等信息,值得一提的是long,double长度为64为会占据两个局部变量空间其余为一个。
- 当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值。
mark
- 栈上分配
- 小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
- 直接分配在栈上,可以自动回收,减轻GC压力
- 大对象或者逃逸对象无法栈上分配
每个线程包含一个栈区,栈中只保存基础数据类型和自定义对象的引用(不是对象),对象都存放在堆区中
每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
3.本地方法栈(Native Method Stack)
- 和虚拟机栈所发挥的效果非常相似, 区别在于 是为执行Native 方法 所服务的,HotSpot 直接把本地方法栈和虚拟机栈合二为一
-二.二、线程共享区域
Method Area(方法区) 方法区是堆的逻辑部分。
(这个只是JVM 中的 一个规范设计 每个厂商可能实现不同 本文会尽可能的 使用JDK1.8 的HotSpot 作为讲解)
在 HotSpot中 我们有了新的东西 就是 Metaspace(元空间) 是对 JVM规范中方法区的实现
元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过设置参数来指定元空间的大小。所有线程共享的。
- 保存装置的类信息
- 类的常量池()
- 类的字段,方法信息
- 方法字节码
-
永久区---> (java 8 HotSpot 中已经废除)- 因为容易出现永久代的内存溢出
JAVA 堆 (Heap )全局共享
- 程序开发程序最为密切
- 目的就是存放对象实例
- 根据垃圾回收算法 分为(Minor GC、Full GC)
- 新生代(Eden ,Survivor from ,Survivor To )
- 老年代
- GC的主要工作区间
- 所有线程共享Java堆
直接内存
- 1.4 中加入NIO 后 基于通道(channel) 与缓存 (Buffer) 使用Native 函数 操作内存 通过一个存储在java堆中的DirectByteBuffer对象作为这块内存引用这样能显著提升性能 避免java堆 与Native 堆 来回复制数据
三、java 的内存模型
- 每一个线程有一个工作内存和主存独立
- 工作内存存放主存中变量的值的拷贝
对于普通变量,一个线程中更新的值,不能马上反应在其他变量中 如果需要在其他线程中立即可见,需要使用 volatile 关键字
mark3.1、多线程操作变量模型
mark3.2、关键字volatile
- 保证内存可见性
- 防止指令重拍(有序性)
-
volatile保证操作原子性(volatile 不能保证 原子性 感谢嗯哼kaka指出 当初可能脑残了=。=)
volatile 不能代替锁
一般认为volatile 比锁性能好(不绝对)
选择使用volatile的条件是:
语义是否满足应用
因为多线程操作和volatile两个意思
关键字volatile 效果
public class VolatileStopThread extends Thread{
private volatile boolean stop = false;
public void stopMe(){
stop=true;
}
public void run(){
int i=0;
while(!stop){
i++;
}
System.out.println("Stop thread");
}
public static void main(String args[]) throws InterruptedException{
VolatileStopThread t=new VolatileStopThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
}
//根本停不下来
// 类似效果
@Slf4j
public class VolatileExample extends Thread{
//设置类静态变量,各线程访问这同一共享变量
private static boolean flag = false;
//无限循环,等待flag变为true时才跳出循环
public void run() {
while (!flag){
};
log.info("停止了");
}
public static void main(String[] args) throws Exception {
new VolatileExample().start();
//sleep的目的是等待线程启动完毕,也就是说进入run的无限循环体了
Thread.sleep(100);
flag = true;
}
}
synchronized (unlock之前,写变量值回主存)
final(一旦初始化完成,其他线程就可见)
3.3 指令重拍
指令重拍就是编译器按照理解的优化代码
可能会不按照代码顺序来 不会进行对象依赖的重拍
会重拍对象之间不依赖的进行重拍
最后 保证 整个线程的语义不发生改变
- 线程内串行语义
写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
以上语句不可重排
-
编译器不考虑多线程间的语义
可重排: a=1;b=2;
例子 -----> 正确方法多线程 指令重拍的错 synchronized 锁住对象
mark
3.3.1 指令重排的基本原则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C 那么A必然先于C
- 线程的start方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
编译运行(JIT)
- 将字节码编译成机器码
- 直接执行机器码
- 运行时编译
- 编译后性能有数量级的提升
本文参考文献
- 《深入理解JAVA 虚拟机》
- Java永久代去哪儿了
- 正确使用 Volatile变量
- JVM初探 -JVM内存模型
- 深入探究JVM探秘Metaspace
- 浅析JVM(二)运行时数据区