共计 9707 个字符,预计需要花费 25 分钟才能阅读完成。
原文链接:https://fxbing.github.io/2019…
浏览了《深刻了解 Java 虚拟机》的局部章节,并做了一些简略的笔记,不是很具体,然而能够不便本人查阅。
第三章 垃圾收集器与内存调配策略
1. 对象是否存活
援用计数法:
很难解决对象间的循环援用
可达性剖析
能够作为 GC Roots 的对象:
- 虚拟机栈中援用的对象
- 办法区中类动态属性援用的对象
- 办法区中常量援用的对象
- 本地办法栈中 JNI(Native 办法)援用的对象
2. 援用分类(JDK1.2 实现)
- 强援用:永远不回收
- 软援用 SoftReference(有用但非必须):内存溢出之前回收
- 弱援用 WeakReference(非必须):GC 时回收
- 虚援用 PhantomReference(幽灵援用 / 幻影援用):目标是能在这个对象被收集器回收时收到一个零碎告诉
3. finalize()(不倡议应用)
判断对象是否死亡会经验 两次标记 过程:
①判断是否与 GC Roots 相连;
②执行 finalize()(对象没有笼罩 finalize()或 finalize()曾经被调用过一次时,虚拟机认为没必要执行 finalize(),不会进行第二次标记)。
- 被调用时会放在由虚拟机主动创立的、低优先级的队列 F-Queue 中
- finalize()是对象惟一的自救机会,例如:在 finalize()中将 this 赋值给某个类变量或者对象的成员变量
- 运行代价昂扬,不确定性大、无奈保障各个对象的调用程序
- finalize()能做的所有工作应用 try-finally 或者其余形式能够做得更好、更及时
4. 回收办法区
废除常量
与 Java 堆的回收逻辑相似
无用的类
- 该类的所有实例曾经被回收
- 加载该类的 ClassLoader 曾经被回收
- 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。
5. 垃圾收集算法
(1)标记革除算法
- 效率不高
- 会大量不间断内存碎片
(2)复制算法
- 内存放大为原来的一半
- HotSpot 默认的 Eden 与 Survivor 的大小比例为 8:1
- 没有方法保障每次回收都只有不多于 10% 的对象存活。当 Survivor 空间不够用时,须要依赖老年代进行 调配担保。
调配担保:当 Survivor 空间不能放下上一次 YGC 之后存活的对象时,这些对象间接通过调配担保机制进入老年代。
(3)标记整顿算法
(4)分代收集算法
新生代每次垃圾收集都会有大量对象死去,大量存活,所以采纳复制算法。
老年代对象存活率高、没有额定的空间对它进行担保,必须应用“标记 - 清理”或者“标记 - 整顿”算法。
6. HotSpot 算法实现
(1)枚举根节点
- Java 虚拟机应用精确式 GC(必须确定一个变量是援用还是真正的数据)
- 虚拟机进展之后不须要查看所有的执行上下文和全局的援用地位。
- 类加载实现时计算出什么偏移量上是什么类型的数据,JIT 编译时在特定地位(平安点 )记录OopMap 数据结构(指明栈和寄存器哪些地位是援用)
(2)平安点(SafePoint)
- 程序执行时并非在所有中央都能停顿下来开始 GC,只有达到平安点时能力暂停。
- 平安点选定准则 :是否具备让程序 长时间执行 的特色
- 长时间执行:指令序列复用(如:办法调用、循环跳转、异样跳转)
- 领先式中断(曾经被弃用):所有线程中断,不在平安点的线程复原执行。
主动式中断:在平安点设置中断标记,程序执行到平安点时被动轮询这个标记,判断是否须要进行中断。
(3)平安区域(Safe Region)
- 定义:一段代码中,援用关系不会发生变化。(线程处于 Sleep 或 Blocked 状态)
- 线程执行到 Safe Region 时,标识本人进入 Safe Region 状态;
JVM GC 时不论 Safe Region 状态的线程;
当线程来到 Safe Region 状态时,查看零碎是否实现了根节点枚举或整个 GC 过程;
如果实现了,那线程继续执行,否则,必须期待收到能够平安来到 Safe Region 的信号为止。
7. 垃圾收集器
连线代表能够组合应用。
(1)Serial 收集器(Client 模式下新生代垃圾清理首选)
- 进行垃圾收集时,必须暂停其余所有的工作线程,直到它收集完结。
- Serial/Serial Old 收集器在新生代采纳 复制算法 ,老年代采纳 标记 - 整顿算法。
(2)ParNew 收集器(Server 模式下新生代垃圾清理首选)
- 多线程版本的 Serial 收集器
- 第一款真正意义上的 并发 收集器
- 默认开启的收集线程数与 CPU 的数量雷同
(3)Parallel Scavenge 收集器
- 指标:达到一个可管制的吞吐量。(吞吐量 = 运行用户代码工夫 / (运行用户代码工夫 + 垃圾收集工夫))
- 适宜在后盾运算而不须要太多交互的工作
- 能够设置最大垃圾收集停段时间(-XX:MaxGCPauseMillis)和吞吐量大小(-XX:GCTimeRatio)
- 能够开启 -XX:UseAdaptiveSizePolicy,之后虚拟机依据零碎运行状况主动设置新生代大小、Eden 与 Survivor 比例、降职老年代对象大小等细节参数。(GC 自适应的调节策略)
(4)Serial Old 收集器
(5)Parallel Old 收集器
(6)CMS 收集器
- 四个步骤:
a: 初始标记(进展):记录与 GC Roots 间接相连的对象
b: 并发标记:GC Roots Tracing
c: 从新标记(进展更长):修改并发标记期间因用户程序持续运作而导致的标记产生变动的记录
d: 并发革除 - 对 CPU 资源敏感
- 无奈解决 浮动垃圾(并发革除阶段用户程序产生的新的垃圾,须要等下次 GC 时再进行清理),可能呈现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生。(不能等老年代被填满之后进行清理,须要为并发革除期间用户程序的执行预留空间)
- 空间碎片过多
(7)G1 收集器(JDK1.7)
- 并行与并发
- 分代收集
- 空间整合:从整体上看是基于“标记 - 整顿”算法实现的收集器,从部分(两个 Region 之间)上来看是基于“复制“算法实现的。
- 可预测的进展:使用者能够指定垃圾收集上耗费的工夫不得超过 M 毫秒。
- 调配的对象会记录在 Remembered Set 中,内存回收时再 GC 根节点的枚举范畴中退出 Remembered Set,确保不对全堆扫描也不会有泄露。
- 不计算保护 Remembered Set 的过程,能够分为以下几个步骤:
a: 初始标记
b: 并发标记
c: 最终标记:并发标记期间对象变动记录在线程 Remembered Set Logs 中,该阶段将 Remembered Set Logs 整合到 Remembered Set 中。
d: 筛选回收:依据用户冀望的 GC 进展工夫制订回收打算。
8. 内存调配与回收策略
- 对象优先在 Eden 调配
新生代 GC(Minor GC):十分频繁,回收速度也比拟块。
老年代 GC(Major GC / Full GC):随同至多一次的 Minor GC,比 Minor GC 慢 10 倍以上。
- 大对象间接进入老年代
- 长期存活的对象将进入老年代 (能够通过 -XX:MaxTenuringThreshold 参数进行设置,默认执行完15 次 Minor GC)
- 动静对象年龄判断:如果在 Survivor 空间中雷同年龄所有对象大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就能够间接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
- 空间调配担保 :在进行 Minor GC 之前会执行上面的流程,
a: 查看 老年代最大间断可用空间是否大于新生代所有对象总空间 ,如果是 Minor GC 能够确保安全,否则,执行 b;
b: 查看 HandlePromotionFailure 设置的值是否容许担保失败,如果是,执行 c,否则,执行 Full GC;
c: 查看 老年代最大可用间断空间是否大于历次降职到老年代对象的均匀大小,如果是,尝试一次 Minor GC(可能存在危险),否则,将 HandlePromotionFailure 设置为不容许冒险,改为进行一次 Full GC。
第七章 虚拟机类加载机制
1. 类的加载过程
- 按程序循序渐进的开始(不是完结,一个阶段中调用激活另一个阶段),解析阶段能够在初始化之后再开始(动静绑定)
- Java 虚拟机规定的必须进行 初始化 的 5 种状况:
(1)遇到 new、getstatic、putstatic、invokestatic 这 4 条字节码指令(对应应用 new 关键字实例化对象 、 读取或设置类的动态字段 (被 final 润饰、已在编译期把后果放入常量池的动态字段除外)以及 调用一个类的静态方法 几种状况)
(2)反射调用。
(3)初始化一个类时,如果父类没有进行过初始化,须要先触发父类初始化。
(4)虚拟机启动时,用户须要指定一个要执行的主类(蕴含 main()办法的类),虚构机会先初始化主类。
(5)然而用动静语言反对时,如果一个 java.lang.invoke.MethodHandle 实例后解析后果 REF_putStatic , REF_getStatic , REF_invokeStatic 的办法句柄时,当改办法句柄对应的类没有初始化时,须要初始化该类。
接口初始化时并不要求其父类接口全副都初始化实现,只有在真正应用到父类接口时才会初始化。
有且仅有下面五种状况会触发初始化,成为被动援用,除此之外,其余所有形式都不会触发初始化,称为被动援用。
被动援用举例:
- 通过子类援用父类的动态字段,不会导致子类的初始化。
- 通过数组定义来援用类,不会触发此类的初始化。
- 常量在编译阶段会存入调用类的常量池中,实质上并没有间接援用到定义常量的类,因而不会触发定义常量的类的初始化。
1.1 加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
获取路径:ZIP 包(JAR、EAR、WAR)、网络(Applet)、运行时计算生成(动静代理)、其余文件(JSP 利用)、数据库
- 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区这个类的各种数据的拜访入口
- 非数组类型应用疏导类加载器或者用户自定义类加载器进行加载
- 数组的组件类型(去掉一个维度之后的类型)是援用类型,则依照一般类加载过程加载;不是援用类型,标记为与疏导类加载器关联。
- 数组类可见性与组件类型可见性统一。如果组件类型不是援用类型,可见性默认为 public。
1.2 验证(十分重要但不肯定必要)
- 文件格式验证:保障输出的字节流能正确地解析并存储于办法区之内,格局上合乎形容一个 Java 类型信息的要求。
通过此验证之后字节流才会进入内存办法区,前面 3 个验证阶段都是基于办法区中的存储构造
- 元数据验证(语义剖析):保障不存在不合乎语言标准的元数据信息。
- 字节码验证(并不能齐全保障平安):对类的办法体进行校验剖析,保障被校验类的办法在运行时不会做出危害虚拟机平安的事件。
- 符号援用验证:产生在虚拟机将符号援用转化为间接援用阶段,确保解析动作能够失常执行。
1.3 筹备
为类变量(被 static 润饰的变量,不包含实例变量)分配内存并设置类变量初始值(个别是零值)。
通常状况下初始化零值,如果存在 ConstantValue 属性,则指定为 ConstantValue 属性的值。
public static int value = 123;
此代码 value 筹备阶段之后的后果为 0;public static final int value = 123;
此代码 value 筹备阶段之后的后果为 123。
1.4 解析
将常量池中的符号援用替换为间接援用。
除 invokeddynamic 指令,其余须要进行解析的字节码指令都会对第一次解析后果进行缓存。
解析动作次要针对类或接口、字段、类办法、接口办法、办法类型、办法句柄和调用点限定符七类符号援用进行。
1.5 初始化
执行类结构器 <clinit>()
办法。
- 动态语句块中只能拜访到定义在动态语句块之前的变量,定义在它之后的变量,在后面的动态语句块能够赋值,但不能拜访。
<clinit>()
不须要显示调用父类的<clinit>()
,由虚拟机保障父类<clinit>()
执行。
第一个被执行的<clinit>()
办法的类必定是java.lang.Object
。- 父类中定义的动态语句块优于子类变量的复制操作。
<clinit>()
是非必须的(没有动态语句块和变量赋值操作)- 接口不能应用动态语句块,然而能够对变量赋值。
只有父接口中定义的变量应用时,父接口才会初始化。 - 虚拟机保障一个类的
<clinit>()
办法在多线程环境下被正确的加锁、同步。
2. 类加载器
2.1 判断两个类相等
- 应用雷同类加载器
- 全限定名雷同
2.2 双亲委派模型
双亲委派模型工作过程:
如果一个类加载器收到类加载的申请,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器实现。每个类加载器都是如此,只有当父加载器在本人的搜寻范畴内找不到指定的类时(即 ClassNotFoundException),子加载器才会尝试本人去加载。
类加载器之间的父子关系不会以继承实现,而是应用组合的形式。
双亲委派模型的三次毁坏
- 第一次毁坏是 在 jdk 1.2 之前,用户自定义的类加载器都是重写 Classloader 中的 loadClass 办法 , 这样就导致每个自定义的类加载器其实是在应用本人的 loadClass 办法中的加载机制来进行加载, 这种模式当然是不合乎双亲委派机制的,也是无奈保障同一个类在 jvm 中的唯一性的。为了向前兼容,java 官网 在 Classloader 中增加了 findClass 办法, 用户只须要从新这个 findClass 办法,在 loadClass 办法的逻辑里,如果父类加载失败的时候,才会调用本人的 findClass 办法来实现类加载,这样就保障了写出的类加载器是合乎双亲委派机制的。
- 第二次的毁坏是由模型自身的缺点导致的,根类加载器加载了根底代码,然而根底代码中有可能调用了用户的代码,然而对于根类加载器而言是不意识用户的代码的。
那么这时候 java 团队应用了一个不太优雅的设计:线程上下文类加载器。这个类加载器能够通过 Thread 类的 setContextClassLoader 办法进行设置, 如果创立线程时还未设置,它就从父线程继承一个,如果在利用全局范畴内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
利用这个线程上下文类加载器傅,父类加载器申请子类加载器去加载某些本人辨认不了的类。
java 中根本所有波及 spi 的加载动作基本上都采纳了这种形式,例如 jndi,jdbc 等。
- 第三次的毁坏是因为 用户对于程序的动态性谋求,诸如:代码热替换,模块热部署 。
目前业界 Java 模块化的规范是 OSGI。而 OSGI 实现模块热部署的要害是他本人的类加载机制:每个程序模块 (bundle) 都有本人的类加载器,须要更换程序 (bundle) 的时候,连同类加载器一起替换,以实现代码的热部署。
第八章 虚拟机字节码执行引擎
1. 运行时栈帧构造
- 蕴含 局部变量表、操作数栈、动静连贯和办法返回地址 等信息。
- 编译时确定栈帧中的局部变量表大小。
- 一个栈帧须要调配多少内存不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
- 只有位于栈顶的栈帧(以后栈帧)才是无效的。
1.1 局部变量表
- 以容量槽(Slot)为最小单位
- 每个 Slot 都应该可能寄存一个 boolean、byte、char、short、int、float、reference 或 returnAddress 类型数据。
- 如果一个局部变量定义了然而没有赋初始值是不能应用的
1.2 操作数栈
- Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈
1.3 动静连贯
指向运行时常量池中该栈帧所属办法的援用。
动态解析 :符号援用在类加载阶段或第一次应用时转化为间接援用。
动静连贯:符号援用在每一次运行期间转化为间接援用。
1.4 办法返回地址
办法失常退出时,调用者的 PC 计数器 的值能够作为返回地址,栈帧中很可能会保留这个计数器的值。
办法异样退出时,返回地址通过 异样处理器 表来确定,栈帧中个别不会保留这部分信息。
办法退出过程:
- 复原下层办法的局部变量表和操作数栈
- 把返回值压入调用者栈帧的操作数栈
- 调整 PC 计数器的值以指向办法调用指令前面的一条指令
2. 办法调用
2.1 解析
- 只有能被 invokestatic 和 invokespecial 指令调用的办法都能够在解析阶段确定惟一的调用版本。
- 合乎上述条件的的有 静态方法、公有办法、实例结构器、父类办法 4 种。都称为 非虚办法 。其余办法为 虚办法。
final 办法也是非虚办法。
2.2 分派(与多态个性无关)
动态分派 | 动静分派 |
---|---|
编译阶段 | 运行阶段 |
重载 | 重写 |
多分派(关怀动态类型与办法参数两个因素) | 单分派(只关怀办法接收者) |
(1)动态分派(重载)
所有通过动态类型来定位办法执行版本的分派动作称为动态分派。
- 办法重载是动态分派的典型利用
- 动态分派产生在编译阶段
Human man = new Man();
Human
为变量的 动态类型 ,动态类型的变动仅仅在应用时产生,变量自身的动态类型不会被扭转,并且最终的动态类型是在编译期可知的;Man
为变量的 理论类型 ,理论类型的变动后果在运行期才能够确定,编译期间不晓得对象的理论类型。
重载 通过参数的 动态类型而不是理论类型 作为断定根据。
(2)动静分派(重写)
运行期依据理论类型确定办法执行版本的分派过程称为动静分派。
依据操作数栈中的信息确定接受者的理论类型
(3)单分派与多分派
办法的接收者与办法的参数统称为办法的 宗量 。
单分派 是依据一个宗量对指标办法进行抉择,多分派 则是依据多个宗量对指标办法进行抉择。
动态分派 属于 多分派 , 动静分派 属于 单分派。
(4)多分派的实现
为类在办法区中建设 虚办法表 ,寄存各个办法的理论入口地址。
如果子类重写了父类函数,虚办法表中寄存指向子类实现版本的入口地址;否则,与父类雷同办法的入口地址统一。
第十二章 Java 内存模型与线程
1. Java 内存模型
工作内存 | 主内存 |
---|---|
线程对变量读取、赋值等操作 | 线程间变量值的传递 |
虚拟机栈中的局部区域 | Java 堆中的对象实例数据局部 |
1.1 内存间的交互操作
- lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,开释后的变量才能够被其余线程锁定
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作应用
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中失去的变量值放入工作内存的变量正本中
- use(应用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接管到的值赋给工作内存的变量
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作
- write(写入):作用于工作内存的变量,它把 store 操作从工作内存中的一个变量的值传送到主内存的变量中
同步规定剖析:
- 不容许 read 和 load、store 和 write 操作之一独自呈现,以上两个操作必须按程序执行,但没有保障必须间断执行,也就是说,read 与 load 之间、store 与 write 之间是可插入其余指令的。
- 不容许一个线程抛弃它的最近的 assign 操作,即变量在工作内存中扭转了之后必须把该变动同步回主内存。
- 不容许一个线程无起因地(没有产生过任何 assign 操作)把数据从工作内存同步会主内存中
- 一个新的变量只能在主内存中诞生,不容许在工作内存中间接应用一个未被初始化(load 或者 assign)的变量。即就是对一个变量施行 use 和 store 操作之前,必须先自行 assign 和 load 操作。
- 一个变量在同一时刻只容许一条线程对其进行 lock 操作,但 lock 操作能够被同一线程反复执行屡次,屡次执行 lock 后,只有执行雷同次数的 unlock 操作,变量才会被解锁。lock 和 unlock 必须成对呈现。
- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎应用这个变量之前须要从新执行 load 或 assign 操作初始化变量的值。
- 如果一个变量当时没有被 lock 操作锁定,则不容许对它执行 unlock 操作;也不容许去 unlock 一个被其余线程锁定的变量。
- 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)
2.3 volatile
1. 保障可见性,read 与 load、aggsin 与 store 两两不离开。
2. 禁止指令重排序优化,内存屏障
2.4 long 与 double 型变量的非凡规定
虚拟机不保障 64 位数据类型的 load、store、read 和 write 这四个操作的原子性。
目前的商用虚拟机保障了 64 位数据的类型读写操作的原子性
2.5 原子性、可见性与有序性
(1)原子性
- 根本数据类型具备原子性
sychronized
关键字保障更大范畴内的原子性
(2)可见性
volatile
、sychronized
和final
三个关键字实现可见性。final
关键字的可见性:
被 final 润饰的字段在结构器中一旦初始化实现,并且结构器没有把this
的援用传递进来,那在其余线程中就能看到 final 字段的值。
(3)有序性
volatile
和sychronized
两个关键字实现有序性。- 如果在本线程内察看,所有的操作都是有序的;如果在一个线程中察看另一个线程,所有的操作都是无序的。
2.6 后行产生准则(happens-before)
(1)定义
- 如果一个操作 happens-before 另一个操作,那么第一个操作的执行后果将对第二个操作可见,而且第一个操作的执行程序排在第二个操作之前。
- 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要依照 happens-before 关系指定的程序来执行。如果重排序之后的执行后果,与按 happens-before 关系来执行的后果统一,那么这种重排序并不非法(也就是说,JMM 容许这种重排序)。
(2)具体规定
- 程序程序规定:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
- 监视器锁规定:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 变量规定:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
- start()规定 :如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。
- join()规定 :如果线程 A 执行操作 ThreadB.join() 并胜利返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作胜利返回。
- 程序中断规定 :对线程 interrupted() 办法的调用后行于被中断线程的代码检测到中断工夫的产生。
- 对象 finalize 规定 :一个对象的初始化实现(构造函数执行完结)后行于产生它的 finalize() 办法的开始。
Java 与线程
在 Java 中,JDK1.2 之前由用户线程实现,JDK1.2 之后应用基于操作系统原生线程模型实现,win 和 linux 都是用的一对一的线程模型(一条 Java 线程映射到一条轻量级过程中)。
1. 线程的实现
1.1 应用内核线程实现
- 不间接应用内核线程,而是应用内核线程的高级接口:轻量级过程。
- 轻量级过程与内核线程的数量比为 1:1。
- 轻量级过程耗费内核资源,一个零碎反对的轻量级过程的数量是无限的。
1.2 应用用户线程实现(实现简单,没有应用)
- 过程与用户线程之间是 1:N 的关系
1.3 应用用户线程加轻量级过程混合实现
- 用户线程与轻量级过程之间是 N:M 的关系。
2. 线程调度
- 协同式线程调度:实现简略,但线程执行工夫不可控
- 抢占式线程调度:Java 一共 10 个优先级,但 windows 零碎只有 7 个