深入理解Java虚拟机 读书笔记

前言

​ 最近一直在学游泳,成功的从一个旱鸭子学成了一个蛙泳健将,不错,上月把《深入理解JAVA虚拟机》看完了,一直说整理一下在本子上面的笔记,到现在发现根据遗忘曲线来说,已经忘记得差不多了,现在整理一下回顾一下,加深记忆,在过几个月在回顾,这是一个技巧。

​ 工作上一直就是需求评审,需求研发,测试,改bug,上线,循环下去,但是这周接触到性能方面的问题,这是搬砖过程中难得的一次经验提升的机会,其实最后解决的方法也简单,只是加了Redis缓存而已,但是解决性能问题的过程也是挺有意思的

​ 好了,不说了,开始整理笔记

深入理解JAVA虚拟机

​ 这本书买了很久,一直没有看,也很正常,买了好多书,都没有看,但是最近在看内存,和类加载机制的时候,发现网上的文章很多是来自本书,索性就系统学习一下,网上零碎的知识始终没有详细的了解下来有意思。

​ 这本书是真的不错,值得一读,读完之后,会对JAVA虚拟机有一个全面的了解,不吹了。开始吧。

自动内存管理

运行时数据区域

​ JVM在执行JAVA程序时会将内存分成不同的数据区域

​ 1、程序计数器

​ 字节码的行号指示器,字节码解释器通过计数器的值来选取下一条需要执行的指令

​ 2、Java虚拟机栈

​ Java方法执行的内存模型,每个方法执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接

​ 3、本地方法栈

​ 为本地的Native方法服务

​ 4、Java堆

​ 所有的线程共享的内存区域,存放对象的实例。并且在这分配内存,该区域是垃圾收集器管理的主要区域, 也成GC堆

​ 5、方法区

​ 同Java堆,线程共享的内存区域,用来存加载的类信息,常亮,静态变量

​ 6、运行时常量池

​ 方法区的一部分。static final 修饰常量

​ 运行时常量:运行时确定值

​ 编译器常量:编译器确定值

​ 编译器常量风险:A类常量 a=1

​ B类常量 b=2 B类使用A类 a=1变量 A,B类同时编译,此时a=1 b=2

​ 修改A类 a=3 只编译A类 此时B类的 a=1 Error

​ 7、直接内存

​ 避免Java堆和Native堆来回复制数据,此时是中间内存

垃圾收集器

确定哪些对象还活着或死去

确定方法

引用计数法

给对象添加引用计数器,引用+1 引用失效-1,为0时,即可被回收

缺点:难以解决循环引用

可达性分析算法

GC Root起始点,当对象的引用链不可达到GC Root时,对象可被回收

可用来做GC Root的对象

1、虚拟机栈中引用的变量表

2、方法区中类静态属性引用的对象

3、常量

4、Native方法

引用

JDK1.2之前只有被引用和没有被引用的状态

JDK1.2之后引用区分:

​ 强引用(Strong Reference)

​ 软引用(Soft Reference)

​ 弱引用(Weak Reference)

​ 虚引用(Phantom Reference)

强引用:只要强引用还存在,GC就不会被回收,JAVA虚拟机抛出OOM也不会回收 实现方法 New xx 如果需要回收,则将对象设置为null 例如ArrayList clear方法将elementData[i]=null

软引用:软引用会在内存不足的时候去回收,就是在OOM之前,会去清空对象

​ 一般配合队列使用,下面代码就是配合引用队列使用

1
2
3
4
5
6
7
8
9
10
11
//定义一个引用对象        
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
// Notify GC 通知GC,此时软引用没有被回收,因为被回收之后,引用队列的poll方法返回队列头元素
//打印输出reference为null,表示软引用没有被回收
System.gc();
System.out.println(softReference.get()); // abc
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference); //null

弱引用:相比软引用,拥有更短的生命周期,无论是内存是否足够,都会被回收,使用方法和软引用一样

虚引用:形同虚设,任何时候都会被回收,必须和引用队列联合使用,当垃圾回收器准备回收对象时,会在这之前将虚引用加到引用队列中 。用处:判断对象的垃圾回收

finalize方法

GC在回收对象前调用该方法

任何对象的finalize方法只会被系统调用一次

不建议使用

垃圾收集算法

标记-清除

1、标记需要回收的对象

2、清除标记的对象

缺点:效率,空间:会产生大量不连续的内存碎片,如果需要分配较大的对象则没有空间

复制

1、将内存空间分为两份,不同比例

2、将存活的对象移动到另一块内存区域

3、将已使用的内存区域清理掉

缺点:内存可使用的区域变小

标记-整理

将存活的对象移动到一端,另一端清理屌

分代收集

Java堆分为新生代,老年代。不同年代使用不同算法,新生代有大量对象死亡,少量存活,则可以使用复制算法

老年代对象存活率高,可以使用标记-清理,整理算法

内存分配与回收策略

对象优先分配到Eden区 新生区,当Eden区没有足够的空间,会触发minor GC,新生代GC(minor GC)

如果新生代GC没有可回收对象,采用担保机制就将新生代里面的对象移到老年代去,长期存活的对象进入老年代

-xx:maxTenuringThreshold 熬过一次MinorGc 年龄+1 超过设置的值就移到老年代去 改变这个值的大小,就表示,熬过几次新生代GC就移到老年代去

空间分配担保

MinorGC前检查老年代最大的连续空间是否大于新生代的所有对象空间,如果大于,则MinroGC安全,如果小于,HandlerPormotionFailare 是否允许担保失败,如果允许,检查历次晋升到老年代的对象是否小于老年代最大的连续空间,如果是,则尝试minorGC,如果不允许担保失败,不冒险。则FullGC

垃圾收集器

不同厂商,不同版本的虚拟机所提供的垃圾收集器有很大区别,不同场景,不同应用有最适合的垃圾收集器

serial

新生代收集器,采用标记复制收集算法,单线程,并不是指单线程,而是在一个CPU在执行GC时。必须暂停其它工作线程

serial old

老年代收集器,采用标记整理收集算法,和serial收集器类似,是单线程

parNew

serial收集器的多线程版本,server模式下的新生代收集器,只有CMS收集器能配合工作

(并行与并发的概念)并行 parallel

多条垃圾收集器多线程并行工作,此时用户线程任然处于等待状态

并发 concurrent

值用户线程与垃圾收集器线程同时工作(可能不是并行,可能会交替进行)用户线程继续运行,垃圾收集器线程运行在另一个CPU上

parallel scavenge 收集器

新生代收集器,采用复制算法,并行多线程收集器

关注点:CMS等收集器,缩短GC时用户线程停顿时间。parallel scavenge控制吞吐量

两个参数精确控制吞吐量

-xx:MaxGcpauseMillis 大于0的毫秒数

保证内存回收的时间控制在设定值,GC停顿时间是牺牲吞吐量和新生代的空间换取

-xx:GcTimePatio 0-100整数

垃圾收集所暂用总时间的比例

parallel old收集器

老年代收集器,采用标记整理算法

CMS收集器

CMS (cancurrent Mark sweep)

获取最短回收停顿时间为目标的收集器,互联网站或B/S系统适用,提升响应速度,系统停顿时间最短

Mark sweep 标记-清除

整个过程4个步骤

初始标记 CMS initial mark 需要STW (stop the world)

并发标记 CMS concurrent mark

重新标记 CMS rement 需要STW (stop the world)

并发清除 CMS concurrent sweep

初始标记,重新标记人需要stop the world

初始标记只需要标记GC Root能关联的对象,并发标记就是GC Root跟踪的一个过程

重新标记则是记录在并发标记期间,用户继续运作产生变动的一部分对象,停顿时间比初始标记长,远比并发标记段

整个过程耗时最长的并发标记和并发清除是可以和用户线程一起工作

CMS收集器内存回收可以与用户线程一起工作

CMS收集器特点:并发收集,低停顿

缺点:1、对CPU资源敏感

并发回收时,占用CPU资源的25%以上,随着CPU数量的增加而下降,当CPU是2个回收时,占用资源达到50%

2、无法处理浮动垃圾

并发清理阶段,用户线程还会产生新的垃圾(浮动垃圾)

3、CMS基于标记-清除算法实现,会产生大量的空间碎片

分配大对象会触发FullGC

+usecmscompactAtFullCollection 默认开启

在要FullGC时会整理内存空间,空间碎片没了,停顿时间会变长

G1收集器

特点:并行 并发 分代收集 空间整理

G1从整体上看是基于标记整理算法。G1将Java堆分为若干Region ,标记整理Region

从局部来看是基于复制算法(两个Region)

可预测的停顿

G1收集器之前,收集范围都是整个新生代和老年代,G1在堆得布局与其他有很大区别

将Java堆分为多个大小相等的独立区域 Region,根据各个region的可回收的垃圾价值决定所有可预测停顿的时长,G1收集器化整为零 将Java堆分为多个大小相同的region

G1收集器运行步骤:
初始标记,并发标记,最终标记,筛选回收 类似CMS回收步骤,筛选回收则是根据Region的可回收价值来针对回收

虚拟机执行子系统

虚拟机类加载机制

类加载时机

生命周期

加载->验证->准备->解析->初始化->使用->卸载

加载、验证、准备、初始化、卸载这5个阶段顺序是确定,解析为支持Java动态绑定而不规定什么时候进行

只有5中情况必须对类立即进行初始化,包括初始化之前的加载,验证,准备阶段

1、new 读取或设置一个静态字段,以及调用静态方法

2、对类进行反射时

3、初始化一个类时发现父类还没有初始化,则先需要初始化父类

4、虚拟机启动时的主类,包含main的类

5、方法句柄所对应的类没有进行初始化,则需要先触发其初始化

有且只有上述5种方法称之为主动引用

3种被动引用

1、通过子类引用父类的静态字段,不会导致子类初始化 P211代码

2、通过数组定义来引用类,不会触发初始化 P212代码

3、常量在编译阶段会存入调用类的常量池中,本质并没有直接引用定义的常量的类,不会触发定义常量的类的初始化 P213代码

接口与类不同,接口并不要求其父接口全部完成初始化,只有在使用到父接口中定义的常量是才会初始化

类加载过程
加载

1、通过类的全限定名来获取此类的二进制流

2、将字节流所代表的的静态存储结构转换为方法区的运行时数据结构

3、在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口

获取Class文件来源

  1. ZIP包中获取 最常见
  2. 网络中获取
  3. 运行时计算生成 动态代理
  4. 有其他文件生成 有jsp文件生成的class类
  5. 有数据库读取

可重写loadclass()方法自定义不同的类加载器

验证

确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全

4个检验动作

文件格式验证,元数据验证,字节码验证,符号引用验证

1、文件格式验证

验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理

验证点:是否以魔数开头,主次版本号是否在当前虚拟机的处理范围,常量池中的常量是否有不被支持的常量类型,是否有指向不存在的常量,或不符合类型的常量,Class文件中各部分以及文件本身是否有被删除或附加其他信息

2、元数据验证

是对字节码描述的信息进行语义分析确保指定的信息符合Java语言规范

验证点包括:

  1. 是否有父类(除了Object外,所有类都有父类)
  2. 这个类是否集成了不允许被继承的类(被final修饰)
  3. 如果不是抽象类,是否实现了父类或接口中要求实现的所有方法
  4. 类中的字段方法是否与父类产生矛盾,比如覆盖了父类的final字段

3、字节码验证

最重要的验证环节,通过数据流和控制流分析,确保语义合法,符合逻辑,针对方法体进行校验分析,保证方法运行时不会做出危害虚拟机的安全事件

4、符号验证

符号引用验证看做是对类自身以外的信息进行匹配校验

准备

准备阶段是正式为类变量分配内存并设置类变量初始值阶段,此时分配内存仅分配被static修饰的变量,设置默认值只是设置value的零值

解析

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量

直接引用:可以直接指向目标的指针,相对偏移量,或是一个能间接定位到目标的句柄

解析阶段就是虚拟机将常量池内的符号引用转换为直接引用的过程,解析动作主要针对类和接口,字段,类方法,接口方法,方法类型,方法句柄

初始化

类加载过程中最后一步,初始化阶段为类的静态变量赋予正确的初始值

类加载器

类和类加载器

任意一个类需要由他的类加载器和类的本身确定在虚拟机中的唯一性,比较两个类是否相等,只有在两个类都是由同一个类加载器的前提下比较才有意义

双亲委派模型

双亲委派模型要求:

除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器

双亲委派模型工作过程:

如果一个类加载器收到了类加载器的请求,他首先不会自己去尝试加载这个类,而是把请求委派给父类去加载,每个层次的类加载器都是如此,因此所有的加载请求,最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(搜索范围内没有所需的类)时,自家在其才会尝试自己加载

双亲委派模型好处:
带有优先级的层次关系,保证子类加载器不会和父类加载器加载到重复的类,从而保证类的唯一性

双亲委派模型的实现:

P232代码

Java内存模型

主内存和工作内存

Java内存模型规定所有的变量都存在主内存上,每条线程还会有自己的工作内存,工作内存保存了被该线程使用到的变量的主内存拷贝,线程的所有操作都在工作内存完成,不能直接读写主内存中的变量,不同线程无法直接访问对方的工作内存

内存间相互调用

主内存和工作内存的相互操作

lock 锁定:作用于主内存,把一个变量标识为线程独占

unlock 解锁:作用于主内存,把一个锁定的变量解锁

read 读取:作用于主内存,把主内存的值传输到工作内存,以便随后的load操作

load 载入:作用于工作内存,把read操作从主内存得到的变量放入工作内存的副本

use 使用:作用于工作内存,将工作内存中的一个变量传递给执行引擎,虚拟机需要使用变量时会执行这个操作

assign 赋值:作用于工作内存,将执行引擎街道的值赋值给工作内存

store 存储:作用于工作内存的变量,把工作内存的变量值传递到主内存,以便随后的write操作

write 写入:作用于主内存,把store操作从工作内存得到的变量值放入主内存

Java内存模型规定了执行上述8种基本操作时,必须满足一下规则来保证内存操作在并发条件下时安全的

  1. 不允许read和load,store和write操作之一单独出现,即变量存主内存读取了但工作内存不接受,或从工作内存发起了回写,但主内存不接受

  2. 不允许一个线程丢弃他最近的assign操作,即变量在工作内存中改变了,必须把该变化同步回主内存

  3. 不允许一个线程无原因的(没有assign操作)把数据从工作内存同步到主内存

  4. 新的变量只能在主内存中”诞生“,不允许在工作内存中直接使用一个未被初始化,load,assign的变量,对变量实施use,store操作前必须执行了assign赋值和load载入操作

  5. 一个变量在同一时刻只允许一条线程对其进行load操作,但lock操作可以被同一条线程重复执行多次,多次lock之后,只有执行相同次数的unlock操作,变量才会被解锁

  6. 对变量执行lock操作会清空工作内存中此变量的值,在使用时需要重新执行load或assign初始化变量的值

  7. 如果变量没有被lock操作锁定,不允许unlock操作,也不允许unlock一个被其他线程锁定的变量

  8. 对变量unlock操作之前,必须把变量同步回主内存中执行store,write操作

volatile型变量的特殊规则

变量定义为volatile之后有两种特性

1、保证此变量堆所有线程的可见性

​ 可见性是指当一个线程修改了变量的值,新值对于其他线程时立即可知的,但不能保证变量的原子性,因为Java里面的运算并非原子操作。

​ 在必要时还是需要通过加锁来保证原子性:1、运算结果不依赖变量当前的值或者确保只有单一线程修改变量的值。2、变量不需要与其他变量共同参与不变约束

2、禁止指令重排

普通变量仅会保证结果正确,不能保证赋值操作的顺序与程序代码中的执行顺序一致,简单的理解为禁止虚拟机优化字节码的顺序排列而导致结果异常

Java内存模型对Volatile变量定义的特殊规则

T线程 V,W为volatile变量

1、只有T执行的前一个动作是load载入时,线程T才能对变量执行use使用操作,并且T对V执行的后一个动作是use使用时,T才能对v执行load操作。

T对V的use动作可以认为T对V的load,read动作相关联,必须连续一起出现,这条规则要求在工作内存中,每次使用V时都必须从主内存刷新最新的值,保证能看见其他线程对变量V的操作

2、T对变量V的前一个动作是assign赋值时,T才能对变量V执行store存储动作,并且T对V执行的后一个动作是store时,T才能对V执行assign,T对V的assign操作可以认为T对V的store,write相关联,必须连续出现,保证其他线程可以看到自己对V的修改

3、保证代码的执行顺序与程序时相同的

long double特殊规则

64位数据类型,long double

允许虚拟机将没有被volatile修饰的变量的64位数据读写操作划分为两次32位操作,即虚拟机可以选择不保证64位数据类型的load,store,read,write4个操作的原子性,即long,double的非原子性协议,由于商用虚拟机都选择把64位数据读写操作作为原子操作,因此不需要把long,double变量专门声明位volatile

原子性,可见性,有序性

原子性

read,load,assign,use,store,write

基本数据类型的访问读写是具备原子性,lock unlock也是,但并未开放给用户使用,但提供了更高层次的字节码指令 monitorenter和monitorexit,反映到Java代码就是synchroized

可见性

指当一个线程修改了共享变量的值,其他线程能立即知道修改,无论是普通变量还是volatile变量都是变量修改后将新值同步回主内存,读取变量前从主内存刷新变量值,普通变量和volatile的区别是volatile的特殊规则保证了新值能立即同步回主内存,出volatile之外,Java还有两个关键字实现了可见性 synchroized和final,synchroized是由对一个变量的unlock操作之前必须把此变量同步回主内存执行store,write操作,final可见性,不能更改

有序性

Java程序中天然的有序性可以总结为一句话:如果本线程内观察多有操作都是有序的,如果在一个线程中观察另一个线程所有操作都是无序的。前一句:线程内表现为串行的语义,后一句:指令重排序,工作内存和主内存同步延迟,volatile本身就包含禁止指令重排序,synchreized是一个时刻只允许一个县城对其进行load操作

先行发生原则 happens-before

他是判断数据是否存在竞争,县城是否安全的主要依据

天然的先行发生原则:

1、程序次序规则:在一个线程内按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作,准确的说是控制流顺序,而不是程序代码的顺序,因为要考虑到分支循环操作

2、管道锁定规则:在一个unlock操作先行发生于后面对同一个锁的lock操作

3、volatile变量规则:对一个volatile变量的写操作先行发生于后面的这个变量的读操作

4、线程启动规则:Threa对象的stat方法先行发生于此线程的每一个动作

5、线程终止规则:线程中的所有操作都先行发生于此线程的终止检测,通过Thread.join方法结束

6、线程终端规则:对线程的interrupt方法调用先行发生与中断线程的代码检测到的中断事件的发生 ,通过Thead.inrerrupt方法检测到是否有中断发生

7、对象终结规则:一个对象的初始化完成(构造函数)执行结束先行发生于他的finalize方法的开始

8、传递性:如果动作A先行发生与动作B,动作B先行发生于动作C,可以得出动作A先行发生于动作C

上述可以得出一个结论,时间先后顺序与先行发生原则之间基本没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准

Java线程状态转换

Java定义了5种线程状态在任意时间点,一个线程只能有且只有一种状态

新建 new :创建尚未启动的线程处于这种状态

运行 Runable 处于此线程有可能正在执行,也有可能正在等待CPU为他分配执行时间

无限期等待:waiting处于这种状态的线程不会被分配CPU,执行时间要等待其他线程的显示唤醒,以下方法会让线程陷入无限期等待状态:没有设置Timeout参数的Object.wait,没有设置Timeout参数的Thread.join,LockSupport.park方法

限期等待 (Time,waiting):不会被分配CPU执行时间,无需等待被其他线程显示的唤醒,在一定时间之后由系统自动唤醒,在一定时间之后由系统自动唤醒,以下方法会让线程进入限期等待状态:Thread.sleep,设置了Timeout参数的Object.wait,设置了Timeout参数的Thread.join,Locksupport.parkNans方法,LockSupport.parkuntil方法

阻塞 Blocked:阻塞状态与等待状态的区别是:阻塞状态在等待获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生,等待状态则是在等待一段时间或者唤醒动作发生时,在线程等待进入同步区域时线程将进入这种状态

结束 Terminated:已终止线程的线程状态,线程以结束运行

线程安全程序由强至弱来排序分为5类

1、不可变:被final修饰的变量,因为其不可变,所以永远不会看到多个线程之中处于不一致的状态,不可变带来的线程安全性是最简单最纯粹的

2、绝对线程安全:被sync修饰的,尽管效率低,但确实是安全的

3、相对线程安全:相对安全就是我们通常意义上所讲的线程安全,大部分线程安全都属于这种类型,Vector,HashTable

4、线程兼容:线程兼容是指对象本身并不是线程安全,但是可以通过调用端正确的使用同步手段来保证对象在并发环境中的安全,hashMap,ArrayList

5、线程对立:线程对立是指无论调用端是否采取了同步措施,都无法在多线程中并发使用的代码,应避免出现这种情况

总结

​ 整理了关于这本书的读书笔记,挺不错的,都是理论知识,花了一个多月时间,对JVM大致有了一个了解,虽然工作中涉及到的不多,但在以后的面试中,或者以后工作中,对于Java的深入会有一定帮助,多读书总是没错的。

​ 最近工(划)作(水)中随便学习了一些零乱的东西,比如myabtis的二级缓存,Redis的pipline管道等,总之很杂乱,但是看过就忘了,所以以后要将零碎的东西也整理一下,至少看过了,做个笔记,时间久了,会看到这一路走来,接触的东西有一个总结

​ 至于工作嘛!!!老样子,开会,研发,测试,没什么惊喜,但是有个感触,在和大数据对接的过程中,大数据研发工程师是武大研究生,好生羡慕,在对话的过程中,发现差距还是很大的,他说的redis的pipline,在之前没听说过,所以我才去研究,以及一些在我看来的高端术语,听得我一脸懵逼。哎,加油吧!

​ 面朝大海,春暖花开!

-------------本文结束感谢您的阅读-------------
0%