深入理解JAVA虚拟机--读书笔记

时间:2022-12-27 16:50:14

深入理解JAVA虚拟机  ----JVM高级特性与最佳实践

作者:周志明

读后感:这本书不用我多说,很多人推荐,自己读完后也同样感觉很好,做java相关的朋友强烈推荐。以下是我的读书笔记,记下了一些我关注的要点,多数是摘要,有的是自己组织的语言,如有不妥,请指正。

1.jvm 内存结构

1)程序计数器
较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器;每条线程独立;
2)java虚拟机栈
描述java方法执行的内存模型,即每个方法在执行的同时都会创建一个栈帧用户存储局部变量表、操作数栈、动态链接、方法出口等信息;即一个方法的调用直至完成的过程对应着一个栈帧的入栈到出栈的过程。同样是线程私有;
3)本地方法栈
对应虚拟机使用的native方法,类似java虚拟机栈;
4)java 堆
虚拟机启动时创建,几乎所有对象实例以及数据都要在堆上分配(除jit,逃逸分析等优化技术外),大小可扩展,通过-Xmx和-Xms参数控制;线程共享;
5)方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
6)运行时常量池
可看作方法区的一部分,存放编译期(并不一定只有编译期)生成的各种字面量和符号引用
7)直接内存
可看做是堆外内存,不受java堆大小限制;

以上分类我自己这样分类,便于理解:栈(虚拟机栈和本地方法栈),堆,方法区(常量池,永久代?),程序计数器,直接内存;

2.gc回收

1)收集算法:
引用技术算法:引用加1,引用失效减1;很难解决对象之间循环引用的;
可达性分析算法(java 主流实现):当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用;
标记-清除算法:所有算法的基础,分为“标记”和“清除”两个阶段;效率低,产生大量不连续内存碎片;
复制算法:将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完后,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。实现简单,运行高效,浪费空间;
标记整理算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存;
分代收集算法:(当前商业虚拟机的垃圾收集都采用),一般分为新生代和老年代,新生代只有少量存活对象,使用“复制算法”收集,老年代,存活率高,使用“标记-清理”或者“标记-整理”算法来回收。

回收方法区:或称永久代,性价比较低,反射,动态代理,动态生成jsp等要关注此区域;

2)内存分配
大方向讲就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,少数情况可能直接分配在老年代中,分配规则不一定是百分之百,取决于哪种垃圾回收器和虚拟机相关参数。

3.类加载机制

1)加载时机,生命周期:加载-->验证-->准备-->解析(此步骤循序不一致)-->初始化-->使用-->卸载
初始化的场景:遇到new,getstatic,putstatic,invokestatic4条字节码;使用java.lang.reflect包进行反射调用;初始化一个类先要初始化父类;执行main方法的类;jdk1.7动态语言时,句柄是REF_getStatic,REF_putStatic,REF_invokeStaticde;
2)类加载过程(以下的定义是截取摘要)
加载:获取类的二进制字节流,将静态存储结构转化为方法区的运行时数据结构,内存中生成一个代表这类的java.lang.Class对象;
验证:保证虚拟机安全,文件格式验证、元数据验证、字节码验证、符号引用验证
准备:正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中分配。
解析:虚拟机将常量池内的符号引用替换为直接引用的过程,
初始化:执行类构造器<clinit>()方法的过程(改方法为线程安全)。包含静态变量、构造行数
3)类加载器
知识点:判断两个类相等(包括equals,isAssignableFrom(),isInstance(),instatceof判断所属关系),只有这两个类是由同一个类加载器加载的前提才有意义。
双亲委派模型:启动类加载器<--扩展类加载器<--应用程序类加载器<--自定义类加载器
工作过程:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
见插图
破坏双亲委派模型:1)线程上下文加载器(Theard Context ClassLoader),java.lang.Thread类的setContextClassLoader();2)热加载,OSGi

4.类编译

前段编译把*.java文件转变成*.class文件的过程;后端编译把字节码转变成机器码的过程(JIT);或静态提前编译器直接把*.java文件编译成本地机器码的过程。
语法糖:泛型与类型擦除、自动装箱、拆箱与遍历循环、条件编译;

(晚期)HotSpot内置两种即时编译器:Client Compiler 和Server Compiler (client模式和server模式)
这里主要编译热点代码,热点探测主要有2中方法:基于采样的热点探测(栈顶方法)和基于计数器的热点探测;
编译器的优化技术有:
1)语言无关的经典优化技术之一:公共子表达式消除;
2)语言相关的经典优化技术之一:数组范围检查消除;
3)最重要的优化技术之一:方法内联;(简单理解把目标方法的代码”复制“到发起调用的方法之中,便面发生真是的调用;很复杂的过程啊:))
4)最前沿的优化技术之一:逃逸分析;(JDK1.6)(优化:栈上分配、同步消除即消除变量的同步措施、标量替换:标量是指一个数据已经无法再分解成更小的数据来表示了,如原始数据类型)
  方法逃逸:当一个对象在方法中定义后,他可能被外部方法所引用,如作为调用参数传递到其他方法中;
  线程逃逸:被外部线程访问到,如复制给类变量或者可以在其他线程中访问的实例变量;

如果一个对象没有逃逸,可对其进行高度优化;如:栈上分配;同步消除;标量替换(对象拆解);

逃逸分析:当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。(来自互联网)

5.并发

5.1 java内存模型
主内存与工作内存:java内存模型规定所有变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

5.2内存间交互操作:java内存模型定义了8种原子操作,jvm要报保证每一个操作为原子操作,
lock,(锁定,作用于主内存的变量)
unlock,(解锁,作用于主内存的变量)
read(读取,作用于主内存),
load(载入,放入到工作内存中),
use(作用于工作内存的变量),
assign(赋值,作用于工作内存),
store(存储,作用于工作内存的变量);
write(写入,作用于主内存的变量)
【知识点】如果要把一个变量从主内容复制到工作内存,那就要顺序地执行read和load操作,如果把变量从工作内存同步回主内存,就要顺序地执行store和write操作。注意java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。
以上8中操作必须满足的规则:
1)不允许出现read和load,store和write操作之一单独出现
2)不允许一个线程丢弃他的最近的assign操作
3)不允许一个线程无原因的把数据从线程的工作内存同步到主内存中
4)一个新变量只能在主内存中”诞生”,不允许在工作内存中直接使用一个未被初始化的变量
5)一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会解锁;
6)如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始化变量的值;
7)如果一个变量事先没有被lock操作锁定,那就不允许对他执行unlock操作
8)对一个变量执行unlock之前,必须先把变量同步会主内存(执行store,write);

5.3 volatile
volatile--java虚拟机提供的轻量级的同步机制。2个重要特性。1是保证此变量对所有线程可见,2是禁止指令重排序优化,
1)每次使用之前都要先刷新,执行引擎看不到不一致的情况,保证可见性;但volatile变量在各个线程的工作内存中不存在一致性问题(也可以存在不一致的情况,因为每次使用前要刷新,看不到不一致的地方),但java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。(java内存模型规定,load和use动作连续,store和write动作连续)
2)指令重排从硬件上来讲,指令重排序指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。
性能方面:volatile的读操作的性能消耗与普通变量几乎没有差别,但是写操作则可能会慢一些,因为他需要在本地代码中插入许多内存屏蔽指令来保证处理器不发生乱序执行。
安全性方面:
以下场景仍需要同步:
1)运算结果并不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值;
2)变量不需要与其他的状态变量共同参与不变约束;

5.4 long和double
非原子协定,64位数据类型,load,store,read和write操作可以不保证原子性。(目前商用jvm的实现几乎都选择把64位数据的读写操作作为原子操作来对待)

5.5 原子性、可见性和有序性(总结)
原子性:read,load,assign,use,write
可见性:java内存模型是通过变量修改后将新值同步回主存,在变量读取前从主存刷新变量值这种依赖主存作为传递媒介的方式来实现可见性的。volatile,synchronized,final(this引用逃逸除外;)
有序性:如果在本地线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的;



5.6 先行发生原则
指的是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存*享变量的值、发送了消息、调用了方法等。
java内存模型下的“天然的”先行发生关系:
1)程序次序规则:一个线程,代码顺序(控制流顺序);
2)管程锁定规则:
3)volatile变量规则:写操作先行发生于后面的读操作
4)线程启动规则:start()方法先行发生于此线程的每一个动作;
5)线程终止规则:join
6)线程中断规则:
7)对象终结规则;初始化先于finalize();
8)传递性;

线程状态:
1)新建new
2)运行runable
3)无期限等待waiting
4)期限等待timed waiting
5)阻塞blocked
6)结束terminated

6.线程安全

线程安全的“安全程度”有强至弱来排序:
1)不变性,final
2)绝对线程安全,(在java API中标注自己是线程安全的类,大多数都不是绝对的线程安全)
3)相对线程安全,保证这个对象单独的操作时线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。eg:Vector,HashTable;
4)线程兼容,对象本身并不是线程安全的,但可以通过调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用;eg:arraylist和hashMap;
5)线程对立

线程安全的实现方法:
1)互斥同步(阻塞同步):互斥是因,同步是果;互斥是方法,同步是目的;
java.util.concurrent.ReentranLock,比synchron增加了一些高级特性:等待可中断、可实现公平锁、以及锁可以绑定多个条件;
2)非阻塞同步:(通俗的说就是不断地重试,直到成功为止),乐观的并发策略,需要硬件指令集的支持;
常用指令集:
测试并设置Test-and-Set
获取并增加Fetch-and-Incremet
交换Swap
比较并交换CAS Compare-and-Swap  (漏洞ABA问题)
加载连接/条件存储Load-Linked/Store-Conditional
3)无同步方案:可重入代码,线程本地存储(ThreadLocal)

锁优化
1)自旋锁与自适应自选(cas)
2)锁消除(判定依据是逃逸分析),String类,字符串相加,JDK1.5之前转化为StringBuffer类(线程安全);JDK1.5及以后,之后会StringBuilder
3)锁粗化,范围扩大
4)轻量级锁,jDK1.6加入,
5)偏向锁