做网站付多少定金优化seo厂家
开启新的篇章。 进入JVM啃大瓜系列~~
目录
- 什么是JVM
- 程序的编译执行过程
- 类的生命周期
- 1、加载类Test(Load the Class Test)
- 2、链接Test类: 检验、准备 、解析((可选) Link Test: verify、prepare、resolve
- 3、 初始化Test类: 执行初始化构造器
- 4、 执行 Test.main方法
- 5、 卸载
- JMM内存模型
- JMM模型下的线程间通讯
- volatile
- 原子性与非原子性
- volatile原理
- 参考文献
什么是JVM
要了解什么是JVM,就必须提及JDK。
我们都知道Java具有跨平台的特点,一次编译,到处运行。其可移植性就是基于JDK实现的。
JDK包括JVM和屏蔽操作系统差异的组件
- 各个操作系统的JVM是一样的
- 屏蔽操作系统差异的组件,在各个PC上各不相同。作用是将相同操作对应到不同操作系统的不同操作指令上。
因此不同系统版本的JDK的差异主要是 屏蔽操作系统差异的组件。
程序的编译执行过程
类的生命周期
JVM执行java文件的过程包括: 加载类 > 链接 > 初始化 > 使用 > 卸载
1、加载类Test(Load the Class Test)
最初执行类Test的main方法时,当在JVM中发现没有包含这个类的二进制表示时,JVM会使用类装载器(class loader)尝试查找这样的二进制(class文件)表示文件。如果这个过程失败,便会抛出错误(error)。
通俗地来说,就是讲将硬盘中的class 文件加载到JVM内存中。
2、链接Test类: 检验、准备 、解析((可选) Link Test: verify、prepare、resolve
在加载Test之后,必须在调用main方法之前初始化Test类。在初始化前,Test必须进行链接。链接包括以下三部分:
检验(verification): 检验过程使用一张正确的符号表(a symbol table)检查是否合乎Java编程语言、JVM的语义规范。如果在检验阶段检查到问题,就会抛出错误(error)。 通俗地将,校验阶段就是进行语义性校验。
准备(preparation)
准备工作包括静态存储(static storage)的分配并赋予初始默认值(比如static int num = 24; 会在准备阶段将num赋值为0)和 java虚拟机内部使用的任何数据结构,比如方法表。
解析(Resolution)
解析是检查从Test类到其他类和接口的符号引用的过程。通过加载在Test中提到的其他类和接口,并且检查其引用是否 正确。
一种实现是只有当符号引用被主动使用时,才会去加载它。这是解析的懒惰形式。如果Test类有其他类的符号引用,那么引用只在被使用时才被解析,如果不用到则不解析。
通俗地讲,该阶段就是把类中的符号引用转为直接引用。
举个栗子:
在前期阶段,还不知道类的具体内存地址,就是用符号"com.johnny.polo.Studnet"代替Student类。此时"com.johnny.polo.Studnet"就称为符号引用。
到了解析阶段,JVM就可以将"com.johnny.polo.Studnet"映射成实际的内存地址,之后急用内存地址来代替Student,这种使用内存地址来使用类的方法叫做直接引用。
3、 初始化Test类: 执行初始化构造器
只有类被初始化了才允许执行该类的main方法。
初始化包括按文本顺序执行的所有类变量的初始化器(class variable initializers)和Test类静态初始化器(static initializers)。 但是在Test类被初始化前,其直接或间接父类都要被初始化。
如果类Test拥有另一个类Super作为其父类,那么Super必须在Test之前初始化,这需要装载、验证和准备Super,还可能涉及递归来解析Super的符号引用。
例如:
static int num = 24;在链接的准备阶段,会将num 赋值为 0。然后在初始化阶段会将0修改为24;
4、 执行 Test.main方法
使用:对象的初始化、对象的垃圾回收、对象的销毁
5、 卸载
类生命周期结束的时机
- 正常结束:即JVM将类卸载出运行时数据区域
- 程序发生异常或错误,jvm被强行终止。
- 程序使用了System.exit)()
- 操作系统发生了异常。JVM运行于操作系统之上。操作系统发生了异常,jvm自然也执行不下去。
JMM内存模型
JMM内存模型从另外角度去分析JVM的内存。
JMM模型下的线程间通讯
线程间的通讯必须通过主内存。如果线程B想与线程A进行通信的话:必须经历一下两个步骤:
- 线程B将工作内存中更新过的共享变量刷新到主内存去。
- 线程A到主内存中去读取线程B已经更新过的共享变量。
具体通过以下8中操作来实现:
1、Lock: 将主内存变量标识为一条线程A独占
2、 Read: 将主内存中的num读入到工作内存中。
3、 Load: 将2中读取的变量拷贝到工作内存的变量副本中。
4、 Use: 把工作内存中的变量副本传递给线程去使用。
5、 Assign: 把线程中正在使用的变量,传递给工作内存中的变量副本。
6、 Store:将工作内存中的变量副本传递到主内存中。
7、 Write: 将变量副本作为主内存中的一个变量进行存储。
8、 UnLock: 解除线程的独占状态。
JVM要求上述8个操作必须是原子性的。但是对于64位的数据类型(double/long)有些非原子性协议。因此可能会出现只读取 (写入)半个数据的情况。解决方案:
1、 商用JDK已经解决了上述原生态JDK的问题。
2、 使用volatile 关键字解决。
volatile
volatile 是JVM提供的轻量级的同步机制。
作用:
1、 防止JVM对long/double等64位非原子性协议进行的误操作。
2、 可以使变量对所有的线程立即可见。(如果某一线程修改了工作内存中的变量副本,那么加上volatile之后,该变量就会立刻同步到其他线程的工作内存中)
3、 禁止指令的“重排序”优化
原子性与非原子性
提到重排序,就必须先了解原子性操作和非原子性操作。原子性操作例如num = 10;非原子性操作例如int num = 10;该语句会被 拆分成int num; num = 10;两条语句执行。
而重排序的对象就是原子性操作,目的是为了提高执行效率,是JVM的优化。
int a = 10; //1
int b; //2
b = 20; //3
int c = a * b; // 4
重排序不会影响单线程的执行结果。因此上述程序在经过重排序之后,可能的执行结果是1,2,3,4或者2,3,1,4
但是这样的优化有时也会带来
一些不想要的效果。
/*** 双重检查式的懒汉式单例模式*/
public class Singleton {private static Singleton instance = null;private Singleton(){ }private static Singleton getInstance(){if( instance == null){synchronized(Singleton.class){if( instance == null){instance = new Singleton();}}}return instance;}}
上述代码可能会出现问题,原因是instance = new Singleton() 不是一个原子性操作,会在执行时拆分成以下动作
1、 JVM会分配内存地址、内存空间。
2、 使用构造方法实例化对象
3、 instance = 第1步分配好的内存地址
根据重排序知识,上述动作的执行顺序可能是1,3,2。如果此时是多线程操作,当A线程执行完1、3,然后B线程进入该方法,由于线程A已经为该对象分配了内存地址, instance != null,会直接返回instance对象。此时如果线程B使用该对象的方法则必然会报错。
解决方案是*禁止1,3,2的重排序,在对象引用加上volatile修饰即可
private static volatile Singleton instance = null;
volatile原理
volatile是通过“内存屏障”防止重排序问题
1、 在volatile写操作前,插入StoreStore屏障
2、 在volatile写操作后,插入StoreLoad屏障
3、 在volatile读操作前,插入LoadLoad屏障
4、 在volatile读操作后,插入LoadStore屏障
注意: volatile 不能保证原子性、线程安全 如果想要保证原子性,
验证如下:
public class TestVolatile {static volatile int num = 0;public static void main(String[] args) throws InterruptedException {//创建100个线程 每个线程执行num加3000次1for(int i = 0; i < 100; i ++){new Thread(() -> {for(int j = 0; j< 3000;j ++)num ++;}).start();}Thread.sleep(1000);System.out.println(num);}
}
原先如果是线程安全,则结果应该是30 0000,但答案是206721。说明volatile 并不能保证线程安全。
原因是num ++并不是一个原子操作。 num++被拆分成两个动作:①、 num + 1, ②、 num =①的结果。
要想保证原子操作,可以使用java.util.concurrent.atomic包中的类,该类能保证原子性的核心在于提供了compareAndSet()方法,该方法提供了cas算法(无锁算法)。
参考文献
JVM 文档阅读
周志民 《深入理解java虚拟机》