- 版 次
- 第1版
- 原版名称
- Undertanding the JVM Advanced Features and Best Practives
- 作 者
- 周志明
- 装 帧
- 平装
- 正文语种
- 简体中文
- 书 名
- 深入理解Java虚拟机
- 出版时间
- 2011年6月27日
- 开 本
- 16开
- 出版社
- 机械工业出版社
- ISBN
- 7111349660, 9787111349662
- 页 数
- 387页
-
2022-05-09 17:16:41
前言
hello,大家好~
之前做好书推荐时,给大家分享过《深入理解Java虚拟机》这本书。
我们先来看一下《深入理解Java虚拟机》(第3版)的豆瓣评分
《深入理解Java虚拟机》这本书相信大家都看过吧,说是每位Javaer必看的一本书不过分吧!不论是你面试还是想要在 Java 领域学习的更深,都离不开这本书籍,需要多看几遍,里面都是干货。今天分享这本书的学习笔记,还没来得及看书的小伙伴可以先浏览笔记,看过书的也可以再学习一下。
这份笔记涵盖全书精华,主要包括自动内存管理和虚拟机执行子系统部分,希望能帮助大家对JVM有更全面的认识。
下面是笔记的部分展示图
笔记领取方式:自行扫二维码免费领取
内容介绍
全书一共13章,分为五大部分:
第一部分(第1章)走近Java
系统介绍了Java的技术体系、发展历程、虚拟机家族,以及动手编译JDK,了解这部分内容能对学习JVM提供良好的指引。
第二部分(第2~5章)自动内存管理
详细讲解了Java的内存区域与内存溢出、垃圾收集器与内存分配策略、虚拟机性能监控与故障排除等与自动内存管理相关的内容,以及10余个经典的性能优化案例和优化方法;
第三部分(第6~9章)虚拟机执行子系统
深入分析了虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎,以及多个类加载及其执行子系统的实战案例;
第四部分(第10~11章)程序编译与代码优化
详细讲解了程序的前、后端编译与优化,包括前端的易用性优化措施,如泛型、主动装箱拆箱、条件编译等的内容的深入分析;以及后端的性能优化措施,如虚拟机的热点探测方法、HotSpot 的即时编译器、提前编译器,以及各种常见的编译期优化技术;
第五部分(第12~13章)高效并发
主要讲解了Java实现高并发的原理,包括Java的内存模型、线程与协程,以及线程安全和锁优化。
全书以实战为导向,通过大量与实际生产环境相结合的案例分析和展示了解决各种Java技术难题的方案和技巧。
《深入理解Java虚拟机》第3版的电子书已经打包好了
笔记领取方式:自行扫二维码免费领取
最后
春招接近尾声,即将远去的“金三银四”今年也变成了“铜三铁四”。
大厂不断缩招,不容忽视的疫情影响,加上不断攀升的毕业生人数,各种需要应对的现实问题让整个求职季难上加难。
在这个异常残酷的求职季,很多人的困惑、面临的问题、面试求职的准备,真的需要一份过来人的经验贴。
看了很多网上的“八股文”,最后还是发现一个大佬整理的资料最全面、最干货、最有学习借鉴意义。
这份资料不仅包含了十多个互联网大厂的面试核心知识点、面经汇总和20个技术栈的资料合集,还分别有技术进阶、项目经验、面试突击不同版块的视频解析。
👇🏻 添加 博主 获取更多资料👇🏻 更多相关内容 -
jvm_book:《深入理解Java虚拟机(第3版)》样例代码&勘误
2021-05-04 23:24:45《深入理解Java虚拟机(第3版)》 广告: 《》: 这是一部以“讨论如何构筑一套可靠的分布式大型软件系统”为主题的免费开源文档,如对您有用,望不吝给个Star 不是广告: 快速环境构建、编译、裁剪、调试: ... -
深入理解Java虚拟机学习笔记借鉴.pdf
2021-11-29 01:36:03深入理解Java虚拟机学习笔记借鉴.pdf -
java虚拟机源码-DeepInJVM:深入理解Java虚拟机第二版
2021-05-21 01:23:03java虚拟机 源码 -
深入理解Java虚拟机_JAVA虚拟机_jvm周志明_
2021-10-02 04:32:59深入了解java虚拟机的运行过程已经底层实现原理 -
深入理解 java 虚拟机
2019-02-08 13:20:19java 虚拟机 -
深入理解Java虚拟机---学习感悟以及笔记
2021-01-27 13:11:11它能干什么,文章从作者得角度带大家深入Java虚拟机相关内容,希望对大家有帮助。这里我们使用举例来说明为什么要学习Java虚拟机,其实这个问题就和为什么要学习数据结构和算法是一个道理,工欲善其事,必先利其器。... -
深入理解java虚拟机的故障处理工具
2020-09-01 03:17:25大家都知道在给系统定位问题的时候,知识、经验是关键基础,...Java开发人员可以在jdk安装的bin目录下找到除了java,javac以外的其他命令。这些命令主要是一些用于监视虚拟机和故障处理的工具,下面来看看详细的介绍。 -
深入理解JAVA虚拟机实用技巧案例讲解.ppt
2022-01-02 23:02:03参考使用,欢迎下载 -
深入理解Java虚拟机(第二版)_java_
2021-09-30 06:34:54深入理解Java虚拟机(第二版) -
深入理解Java虚拟机.xmind
2020-06-30 14:35:36自己看《深入理解Java虚拟机》(第二版)所做的一些笔记。因为个人水平有限,能够理解的也只有前面几章的内容,后面的内容觉得看了也不是很理解,就没有记在里面。希望能对大家有所帮助,也希望能和大家一起进步。 -
深入理解Java虚拟机
2021-01-11 12:46:272.2.2 Java虚拟机栈 线程私有,每个方法需要被执行时创建一个栈帧并压入栈顶,执行完毕出栈,调用其他方法时将其他方法的栈帧压入。 栈帧包括局部变量表、操作数栈、动态链接、方法返回地址、附加信息 2.2....第一章 概述
1.1 JVM相关知识体系
1.2 JVM图例
第二章Java内存区域与内存溢出异常
2.1 概述
2.2 运行时数据区域
2.2.1 程序计数器
线程私有,通过改变计数器的值选取下一条需要执行的字节码指令。执行本地方法(native)时计数器的值为空
2.2.2 Java虚拟机栈
线程私有,每个方法需要被执行时创建一个栈帧并压入栈顶,执行完毕出栈,调用其他方法时将其他方法的栈帧压入。
栈帧包括局部变量表、操作数栈、动态链接、方法返回地址、附加信息2.2.2.1 局部变量表
局部变量表可以存放基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用(reference类型),returnAddress类型(执行一条字节码指令的地址)
这些数据类型在局部变量表中的存储空间以局部变量槽Slot为单位,一个Slot具体大小由虚拟机决定。2.2.2.2 操作数栈
跟教科书里栈模拟计算器类似,两数相加则将两数压栈,遇到操作符则取出两数进行相加,将和再压栈。
不过操作数栈结合了局部变量表对变量进行了存储。public void function(){ int a = 1; int b = 2; int c = a + b; }
流程如下:
- 将1压入操作数栈
- 将1从操作栈取出然后放到局部变量表 (a=1)
- 将2压入操作数栈
- 将2从操作栈取出然后放到局部变量表 (b=2)
- 将1从局部变量表取出放到操作数栈
- 将2从局部变量表取出放到操作数栈
- 两个数据出栈求和,将结果3放到操作数栈 (c = a + b)
- 返回
2.2.2.3 动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有该引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。
Class文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。
这些符号引用一部分在类加载阶段或第一次使用时转化为直接引用,这种转化是静态解析。
另一部分在运行期间转化为直接引用,这部分成为动态链接。2.2.2.4 方法返回地址
有正常完成出口和异常完成出口,如果有返回值则会压入调用方的栈帧。
2.2.2.5 附加信息
取决于虚拟机的实现,如虚拟机规范中没有的信息放到其中。
2.2.3 本地方法栈
线程私有,与虚拟机栈类似,区别是本地方法栈作用域本地方法,会受到平台的影响。
2.2.4 方法区(永久代)、元空间、堆、直接内存
2.2.4.1 Java版本与四者的变化
- JDK6->JDK7,字符串常量池、静态变量从方法区转移到堆中,符号引用转移到本地内存
- JDK7->JDK8,方法区转移到元空间,(主要是类型信息class metadata)
2.2.4.2 方法区
方法区存储的内容:
已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存。- 在HotSpot虚拟机上,JDK8之前,用永久代实现方法区,能够参与GC,但导致容易发生内存溢出。
- 永久代的垃圾收集与老年代捆绑,无论谁满了,都会触发两者的垃圾收集
- JDK8之前,方法区和堆逻辑上隔离,物理内存上连续。JDK8时将方法区移入元空间,位于本地内存,此时方法区和堆在物理和逻辑上都是隔离的。
2.2.4.3 元空间
方法区存在于元空间,元空间不再与堆连续,存在于本地内存,没设置限制参数时只受物理内存限制。元空间不会GC。
存储类和类加载器的元数据信息。2.2.4.4 堆
线程共享
存放:- 对象实例
- 数组
- 字符串常量池(JDK8)
- 类静态变量(JDK8)
2.2.4.5 直接内存
元空间在直接内存,直接内存与NIO有关。
2.2.4.6 运行时常量池
是方法区的一部分。Class文件中有常量池表,用于存放编译器生成的各种字面量和符号引用,这部分将在类加载后存放到方法区的运行时常量池中。
Class文件的符号引用和符号引用翻译出来的直接引用也会存储在运行时常量池中。
运行时常量池是动态的,运行期间也可以放入新的常量。2.3 HotSpot虚拟机对象
2.3.1 虚拟机中对象的创建
- 虚拟机遇到字节码new指令时,先检查能否在常量池中定位到类的符号引用,并检查该类是否被加载、解析、初始化过,若没有,则执行类加载过程,否则执行2
- 类加载检查通过后,虚拟机为新对象分配内存。对象所需内存大小在类加载完成后就可以确定。
两种分配内存方式:
- 指针碰撞:在内存规整的情况下,用指针将使用过的内存和未使用过的内存分开,在分配或释放内存时移动指针。(Serial、ParNew收集器)
- 空闲列表:在内存不规整的情况下,使用过的内存和未使用过的内存加错在一起,则虚拟机需要维护一个列表,记录哪块内存可用。(CMS)
JAVA堆是否规整由垃圾收集器是否能空间压缩整理决定。
指针碰撞可能带来的问题:
对象的创建较为频繁,在并发情况下,在给对象A分配内存时,指针还没有来得及修改,对象B又使用原来的指针分配内存。
解决方法: - 对分配内存的动作同步处理,如CAS加失败重试
- 将内存分配的动作按照线程划分在不同的空间进行,即每个线程在Java堆中预分配一块内存,成为本地线程分配缓冲,一个线程需要分配内存时先在改线程内的缓冲区内分配,本地缓冲区使用光后,分配新的缓存区才同步锁定。
2.3.2 对象的内存布局
对象在堆中存储布局可以划分为三部分:对象头、实例数据、对其填充。
2.3.2.1 对象头
对象头包含两部分信息。
一类是存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID,偏向时间戳,被称为Mark Word,这部分数据长度在32位和64位虚拟机中为32比特和64比特。对象头里的信息与对象自身定义数据无关的额外存储成本,是动态的(为了节省空间)。
举例:在32位的HotSpot虚拟机中,对象未被同步锁锁定的状态下,Mark Word32个比特存储空间的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特存储锁标志位,1个比特固定为0.其他状态下(轻量级锁定、重量级锁定、GC标记、可偏向)下的存储内容如下:存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录 11 GC标记 偏向线程ID、翩跹时间戳、对象分代年龄 01 可偏向 另一部分是类型指针,即对象指向他的类型元数据的指针。虚拟机通过该指针判断对象属于哪个类的实例。 2.3.2.2 实例数据
存储类中定义的各种类型的字段内容。一般情况下父类定义的变量出现在子类之前。
2.3.2.3 对其填充
非必须,占位符的作用。HotSpot虚拟机内存管理要求对象起始地址为8字节的整数倍。
2.3.3 对象的访问定位
寻找对象需要通过虚拟机栈上的对象引用(reference)定位堆中对象的位置,但如何定位由虚拟机实现决定。
两种方式:使用句柄和直接指针
句柄式,Java堆中划分一块内存作为句柄池,对象引用reference中存储的是对象的句柄地址。句柄包含对象的实例数据地址和对象类型地址。
指针式,栈中reference对象引用存储的是对象地址,但无法记录对象类型的地址,所以要在对象数据中有记录对象类型所在的地址。
两者差异:- 指针式比句柄式少一次访问开销
- 对象被移动时,句柄式只改变句柄中的实例数据指针,对象引用reference无需改变。而指针式需要改变对象引用。
第三章 垃圾收集器与内存分配策略
3.1 判断对象是否需要回收
3.1.1 引用计数法
方法:对象中添加引用计数器,引用该对象计数器加一,失效时减一,为0则该对象未被使用。
优点:原理简单,判定效率高
缺点:占用一定额外内存,难以解决循环引用问题
循环引用问题:两个不再被使用的对象相互引用,造成两个对象的计数器都不为0,无法判定是否存活需要回收。3.1.2 可达性分析算法
方法:GC Roots的根对象作为起始节点集,沿着这些对象根据引用关系向下搜索,未被搜索到的对象被视为不可达。
优点:解决了循环引用的问题
缺点:复杂
引用链:沿着引用关系搜索的路径为引用链。
GC Roots对象:- 虚拟机栈中引用的对象(栈帧中的本地变量表)、局部变量、临时变量
- 方法区中静态属性引用的对象(static Object a)
- 方法区中常量引用的对象(final Object a)
- 本地方法栈中JNI引用的对象
- 虚拟机内部的引用,如基本数据类型对象的Class对象、常驻的异常对象、系统类加载器
- 被同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 临时对象(局部回收)
3.1.3 再谈引用
为了能够 内存空间足够时,能保留在内存,垃圾回收后内存仍然紧张则抛弃这些对象 ,JDK1.2后Java对引用进行扩充。下面四种引用强度逐渐减弱。
- 强引用:存在引用赋值则为强引用
- 软引用:用于描述有用但非必须的对象。被软引用关联的对象,在系统发生内存溢出前,将这些对象列为二次回收的范围。
SoftReference<String> s=new SoftReference("asdf");
- 弱引用:描述非必须对象,只能生存到下次垃圾回收为止。用WeakReference实现。
- 虚引用:对象是否有虚引用不会对其生存时间构成影响,也无法通过虚引用获得对象实例。虚引用的作用是对象被收集器回收时收到系统通知。用PhantomReference实现虚引用。
3.1.4 生存还是死亡
可达性分析算法中,至少经过两次标记才能宣告对象死亡。第一次分析后未可达的对象会被第一次标记,随后进行筛选,筛选的条件是对象是否需要执行finalize()方法。
对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则这两种情况都被视为不需要执行finalize()方法。
对象被判定需要执行该方法后,对象被放入一个队列中,并在稍后由虚拟机创建的低优先级线程执行队列中对象的finalize()方法,finalize方法是对象逃脱回收的最后一次机会,可以在方法中重新建立引用。
回收队列中对象的finalize方法不一定执行完才结束,为了防止执行缓慢或者死循环使回收系统崩溃。
finalize如今已经被官方声明为不推荐的语法。3.1.5 回收方法区
- 方法区可以回收,但条件苛刻
- 方法区主要回收废弃的常量和不再使用的类型
- 常量的回收需要判断系统中是否有该常量的引用
- 类的回收需要满足:
- 该类的所有实例已经被回收,不存在该类和类的子类的实例
- 加载该类的类加载器已经被回收(很难达成)
- 该类对应的java.lang.Class对象没有在任何地方引用,无法通过反射访问该类的方法
3.2 垃圾回收算法
3.2.1 分代收集理论
几个假说:
- 弱分代假说:大多数对象生存时间短
- 强分代假说:逃过垃圾收集次数越多的对象约难以死亡
- 跨带引用假说:跨代引用相对于同代引用仅占极少数
分代收集的困难:分代后对象之间的跨代引用
假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可 能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整 个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样
最开始的疑惑是为什么要额外遍历整个老年代的对象,后来才猜测只局限新生代的收集时不会遍历整个GC Roots,只会遍历包含新生代引用的GC Roots,这时新生代里若有老年代的引用,这次垃圾收集会漏掉这些对象,所以要遍历一次老年代的对象。
解决跨代引用问题的方法:在新生代上建立全局的数据结构(记忆集Remembered Set),将老年代划分成若干块,标识有跨代引用的块,以后发生Minor GC时,将包含跨代引用的块的对象加入GC Roots扫描3.2.2 标记清除算法
方法:可达性分析判定是否回收然后进行回收
缺点:效率不稳定,空间碎片化问题
优点:简单3.2.3 标记-复制算法
方法:复制存活的对象到另一块区域,清除原来的区域
缺点:浪费部分空间
优点:解决大量可回收对象时效率低的问题(适用于存活对象少的情况,常用语新生代),无碎片化问题3.2.4 标记-整理算法
方法:存活对象向内存一端移动,然后直接清理边界以外的内存。
缺点:存活对象多时效率低
优点:空间利用率高,无碎片化问题3.3 HotSpot算法细节实现
3.3.1 根节点枚举
所有收集器在进行根节点枚举时都要暂停用户线程,否则分析过程中根节点集合的对象引用还在变化,无法保证分析准确性。
3.3.2 安全点
有一个OopMap的数据结构可以维护存放对象引用的位置
HotSpot没有为每条指令生成OopMap,在特定的位置记录这些信息,被称为安全点。用户线程在指令到达安全点后才能暂停。
两种在垃圾回收时让所有线程跑到安全点后停顿的方式:- 抢占式中断:不需要线程的执行代码主动配合,垃圾收集时系统让所以用户线程中断,若用户线程中断的地方不再安全点上,就恢复执行,一会重新中断,直至安全点。
- 主动式中断:垃圾回收时,不对线程操作,而是设置中断位,每个线程执行时主动轮询该标志,发现中断标志为真时在自己挨近的安全点主动挂起。
3.3.3 安全区域
使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了, 但实际情况却并不一定。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集 过程的安全点。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配处理器时间,典型的 场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走 到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于 这种情况,就必须引入安全区域(Safe Region)来解决。 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任 意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。 当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时 间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全 区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。
3.3.4 记忆集与卡表
3.3.5 写屏障
3.3.6 并发的可达性分析
可达性分析算法要求全过程基于一个能保障一致性的快照中才能够进行分析,意味着过程必须暂停用户线程。
根节点枚举的过程中,对象数量相对堆较少,且有优化方式(OopMap),其带来的停顿短暂且固定。
在堆中遍历对象其停顿时间和堆容量成正比。
三色标记:- 白色:对象未被垃圾收集器访问过,分析的开始阶段所有对象都是白色,分析结束阶段,白色的对象意味不可达。
- 黑色:对象已经被垃圾收集器访问过且对象的所有引用已经扫描过。其实安全存活的,如果有其他对象引用了黑色对象无需重新扫描。黑色对象不会直接指向白色对象。
- 灰色:垃圾收集器已经访问过,但对象上至少存在一个引用还没扫描过。
收集器在对象图上标记颜色同时用户线程修改引用关系,可能出现两种情况:
- 原本消亡的的对象标记为存活–可以容忍
- 原本存活的对象标记为死亡–不可以
产生对象消失问题的条件: - 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
解决方法: - 增量更新:破坏第一个条件,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象 了
- 原始快照:破坏第二个条件,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来 进行搜索
3.4 经典垃圾收集器
图片来源
七个垃圾收集器,连线标识垃圾收集器可以配合使用。3.4.1 Serial收集器
第七章 虚拟机类加载机制
7.1 概述
7.2 类加载时机
7.2.1 概述
- 类加载流程如上图。其中,加载、验证、准备、初始化、卸载这五个阶段顺序是确定的。
- 解析有可能在初始化阶段之后开始
- 加载开始时机不确定,由虚拟机实现
7.2.2 类初始化的六种情况
7.2.2.1 遇到new、getstatic、putstatic、invokestatic字节码指令
- new关键字实例化对象
- 读取或设置一个类型的静态字段(被final修饰,或者在编译器把结果放入常量池的静态字段除外)
- 调用静态方法
7.2.2.2 反射
7.2.2.3 子类初始化
7.2.2.4 虚拟机启动时主类初始化
7.2.2.5 JDK7的动态语言支持
7.2.2.6 JDK8的默认方法
7.3 类加载过程
7.3.1 加载
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
7.3.2 连接-验证
确保Classs文件的字节流中包含信息符合规范要求,防止危害虚拟机安全。
验证阶段会进行下面四个验证动作。- 文件格式验证(魔数开头、版本号、常量类型等)
- 元数据信息语义验证(是否有父类、继承关系是否正确)
- 字节码验证(对类的方法体内的逻辑进行验证)
- 符号引用验证(判断该类是否缺少或被禁止访问它依赖的外部类、方法、字段等资源)
7.3.3 连接-准备
将类中定义的静态变量分配内存并设置初始值。(JDK7在方法区,JDK8在堆)
public static int value = 123;
在准备时会被设置初始值0,在初始化阶段才会设置为123
- 被赋值变量为基本类型或String
- 值为字面量而不是方法的形式
如
static int a = 1; // 准备阶段赋值 static int a = getA(); // 初始化阶段赋值
7.2.4 连接-解析
将常量池内的符号引用转换为直接引用。
常量池:
class文件解析出的二进制字节码里定义了常量池,字节码被加载到方法区后,该常量池变成了运行时常量池。常量池是class文件的一部分,保存编译时确定的数据。常量池包含的内容:
- 字面量
- 文本字符创
- 被声明为final的常量
- 基本数据类型的值
- 其他
- 符号引用
- 类和结构的完全限定名
- 字段名称和描述符
符号引用:
用符号代表所引用的目标,类、方法、变量等完全限定名都可以被当做符号引用。直接引用:
直接指向目标的指针、相对偏移量、间接定位到目标的句柄7.2.5 初始化
- 初始化是执行类构造器<clinit>方法的过程
- <clinit>不是Java代码里编写的,而是Javac编译器的自动生成物。
- 编译期自动收集类中所有变量的赋值动作和静态语句块,按在代码中出现的顺序合并
- 虚拟机保证父类的<clinit>一定在子类<clinit>之间执行,因此在调用子类<clinit>时不需要显式调用父类<clinit>
- 同一个类加载器下,一个类只会被加载一次
7.4 类加载器
7.4.1 类与类加载器
- 对于任意一个类,必须由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性
- 每个类加载器都有独立的类命名空间
- 如果一个Class文件被两个类加载器加载,那么这两个类对象在比较时是不同的。(instanceof、equals等)
7.4.2 双亲委派模型
7.4.2.1 三层类加载器
启动类加载器(Bootstrap Class Loader)
- 是虚拟机自身的一部分
- 加载存放在<JAVA_HOME>\lib目录下的类库
扩展类加载器(Extension Class Loader) - 加载存放在<JAVA_HOME>\lib\ext目录下的类库
应用程序类加载器(Application Class Loader) - 加载用户类路径(ClassPath)上的类库
- 默认的类加载器
用户自定义类加载器
- 通过集成java.lang.ClassLoader实现
7.4.2.2 双亲委派模型
工作过程:
- 一个类加载器收到了类加载请求的时候,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,
- 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,
- 只有当父类在自己负责范围内找不到,子类才会自己尝试去完成加载。
好处: - 最后都是由启动类加载器往下找到第一个合适的类加载器去加载
- 防止相同完全限定名的类被不同的类加载器加载,造成类名相同但实际不等的情况
7.5 Java模块化系统
第十二章 Java内存模型与线程
12.1 概述
12.1.1 每秒事务处理数(Transactions Per Second) TPS
一秒内服务端平均能相应的请求总数,和并发能力密切相关
12.1.2 现代计算机系统加入缓存的原因
计算机的存储设备和处理器的运算速度有几个数量级的操作,IO操作让处理器等待数据,会影响整体速度。
将运算的数据从内存复制到缓存中,处理器和缓存之间的IO比内存之间的IO时间小,整体处理速度快。当运算结束后再从缓存同步到内存中
12.1.3 缓存带来的问题-缓存一致性
多路处理器系统中,每个处理器有自己的高速缓存,且共享同一主存。当这些处理器运算都设计到同一个主存区域时,对主存中的同一个数据可能在各自的缓存中不相同,这时无法判断以谁的缓存数据为准。
12.1.4 并发三种问题
12.1.4.1 可见性
原因: 一个线程修改主内存的值后,其它线程可能还在使用工作内存的副本,导致无法感知值的变化,进而产生bug。
**思路:**需要保证其它线程在主内存的值改变时删掉工作内存该值的缓存,从主内存获取数据。
措施:- volatile关键字
- 内存屏障
- synchronized
- Lock
- final
- 等待
- 线程上下文切换
12.1.4.2 有序性
原因: JVM存在指令重排,实际执行的顺序可能与代码顺序不同,在多线程情况下会有影响。
如何保证有序性:- volatile
- synchronized
- Lock
- 内存屏障
12.1.4.3 原子性
含义: 一个或多个操作全都执行且不被其它操作影响,或者全部不执行
保证原子性:
- 64位机器对基础类型变量的读取和赋值是原子性的
- synchronized关键字
- Lock
- CAS
12.1.5 内存屏障
概念: 内存屏障是一条指令,可以对编译器和硬件的指令重排序进行限制。
12.2 Java内存模型(Java Memory Model)
屏蔽硬件和操作系统的内存访问差异,在各个平台下达到一致的内存访问效果
关注在虚拟机中,将变量存储和取出内存的细节
为了更好的执行效率,Java内存模型没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器对代码执行顺序的优化
JDK5之后才成熟
JMM与计算机硬件架构的关系图:
12.2.1 主内存与工作内存
特点:
-
Java内存模型规定所有变量存储在主内存。
-
每条线程有自己的工作内存,工作内存保存了该内存使用的变量的主内存副本。
-
线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存中的数据。
-
不同线程无法直接访问对方的工作内存的变量。
-
线程间变量传递需要通过主内存完成
主内存、工作内存和虚拟机之间的对应关系关系:
- 主内存可以对应Java堆中的对象实例数据
- 工作内存对应虚拟机栈中的部分区域
12.2.2 内存间的交互操作
讲述了一个变量从主内存到工作内存到计算再到回到主内存的过程
Java内存模型定义了八种操作,每种操作都是原子的、不可再分的
作用位置 功能 lock锁定 主内存 把主内存的变量标识为一个线程独占的状态 unlock解锁 主内存 把主内存的变量解锁 read读取 主内存 把变量的值从主内存传输到工作内存中 load载入 工作内存 将工作内存中read获取的值放入变量副本中 use使用 工作内存 将工作内存中变量的值传给执行引擎(虚拟机遇到需要使用变量值的字节码指令时执行,加减乘除等) assign赋值 工作内存 将执行引擎上的值赋值给工作内存的变量 store存储 工作内存 将工作内存变量的值传送到主内存中 write写入 主内存 将主内存中,store操作获取的值放入主内存的变量里 操作的一些规则:
-
不允许read和load,store和write的操作之一单独出现
-
工作内存的数据发生变化前(没有assign操作)不应该将数据同步回主存
-
不允许线程对其最近的assign操作,即变量在工作内存改变后必须同步回主内存(不太理解)
-
不允许在工作内存中使用(use或store操作)未被初始化的变量(load或assign可以初始化变量),
-
同一个变量同一时刻只允许一个线程对其lock操作,lock可以被同一个线程重复执行多次,多次lock需要同样次数的unlock才可以解锁
-
一个主内存中变量被lock后,所有工作内存中该变量的副本值都会被清除,在执行引擎使用这个变量前,需要重新执行load或assign操作来初始化这个变量
-
无法unlock没被lock的变量,也无法unlock被其线程锁定的变量
-
unlock之前必须先将变量同步回主内存中
插一个想法,一个线程在一个变量上加锁后,其他线程还可以改变这个变量,只要访问变量的时候不去获取它的锁
12.2.3 volatile
volatile变量的两个特性:
- 此变量对所有线程可见,当一条线程修改这个变量的值,其他线程立刻就可以知道这个变量的新值
- 禁止指令重排序优化
volatile在三种特性上的体现:
- 可见性:对于volatile变量的读,总是能看到任意线程对这个变量最后的写入
- 原子性:对于volatile的读、写具有原子性
- 有序性:对于volatile修饰的变量,在读写操作前后加上内存屏障来禁止指令重排序,保证有序性。
volatile读-写的内存语义:
- 读volatile变量:JMM把该线程对应的工作内存该变量副本无效,从主内存读取该变量。
- 写volatile变量:在写入工作内存后,会立即刷新到主内存
volatile实现原理:
- JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的。修改后立即同步回主内存,使用时必须从主内存获取。
- 硬件层面:通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
并发时,该变量参与的代码也有可能线程不安全,通过下面两点保证原子性
比较普遍的就是 i++
- 运算结果不依赖变量当前值,或者确保只有单一线程修改变量的值。
- 变量不需要与其他状态变量共同参与不变约束
指令重排序:
程序运行时,编译器和CPU可能对指令优化,重新排序
性能:
- volatile读性能和普通变量相差不多
- 写可能比正常的慢一点,但比锁开销小
Java内存模型对volatile变量的规则:
设T为线程,V和W为两个volatile变量,在进行操作时要满足一些规则
这一块没明白,见书中12.3.3
12.2.4 针对double和long类型变量的特殊规则
允许虚拟机对没有被volatile修饰的64位数据分两次32位的操作进行,导致long和double的操作可能非原子性
-
深入理解java虚拟机第二版源码-books:技术书籍和博客
2021-06-07 01:31:13深入理解java虚拟机 第二版 源码 声明: 本仓库主要是为了传播编程知识, 感谢以下书籍作者, 如果侵犯您的权益, 诚恳的向您表示歉意, 同时我会删除这些书籍 Statement: Warehouse is mainly to spread programming ... -
深入理解Java虚拟机——Java发展史.xmind
2020-04-04 14:02:53这是自己读《深入理解Java虚拟机》时候用XMind建立的思维导图,目的是为了能够帮助自己整理、梳理相关的知识以及方便自己日后的回顾,帮助自己建立起关于JVM的知识体系,里边也有一些对相关内容的补充,通过备注的... -
诸葛 BAT面试之深入理解Java虚拟机 9
2021-08-30 18:02:01诸葛_BAT面试之深入理解Java虚拟机_9 -
JVM:深入理解Java虚拟机 - 学习笔记
2021-05-10 13:43:58JVM深入理解Java虚拟机 - 学习笔记 -
jvm_code:深入理解Java虚拟机代码片
2021-05-01 08:23:48jvm_code 深入理解Java虚拟机code -
深入理解 Java 虚拟机 - v1.0.pdf
2018-05-13 21:23:42Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种... -
深入理解java虚拟机视频教程
2018-08-01 22:09:18深入理解java虚拟机视频教程,jvm原理,java虚拟机,jvm性能调优,内存模型,gc工作原理,内存分配,类的加载等等视频教程 -
深入理解Java虚拟机:JVM高级特性与最佳实践(第2版) pdf下载-附件资源
2021-03-02 15:04:21深入理解Java虚拟机:JVM高级特性与最佳实践(第2版) pdf下载-附件资源 -
《深入理解Java虚拟机-第3版》 学习笔记,一文涵盖全书精髓
2022-07-02 17:06:05大家好~之前做好书推荐时,给大家分享过《深入理解Java虚拟机》这本书。 我们先来看一下《深入理解Java虚拟机》(第3版)的豆瓣评分。前言
大家好~之前做好书推荐时,给大家分享过《深入理解Java虚拟机》这本书。 我们先来看一下《深入理解Java虚拟机》(第3版)的豆瓣评分。
《深入理解Java虚拟机》这本书相信大家都看过吧,说是每位Javaer必看的一本书不过分吧!不论是你面试还是想要在 Java 领域学习的更深,都离不开这本书籍,需要多看几遍,里面都是干货。今天分享这本书的学习笔记,还没来得及看书的小伙伴可以先浏览笔记,看过书的也可以再学习一下。
这份笔记涵盖全书精华,主要包括自动内存管理和虚拟机执行子系统部分,希望能帮助大家对JVM有更全面的认识。
下面是笔记的部分展示图
点击自行免费领取
内容介绍
全书一共13章,分为五大部分:
第一部分(第1章)走近Java
系统介绍了Java的技术体系、发展历程、虚拟机家族,以及动手编译JDK,了解这部分内容能对学习JVM提供良好的指引。
第二部分(第2~5章)自动内存管理
详细讲解了Java的内存区域与内存溢出、垃圾收集器与内存分配策略、虚拟机性能监控与故障排除等与自动内存管理相关的内容,以及10余个经典的性能优化案例和优化方法;
第三部分(第6~9章)虚拟机执行子系统
深入分析了虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎,以及多个类加载及其执行子系统的实战案例;
第四部分(第10~11章)程序编译与代码优化
详细讲解了程序的前、后端编译与优化,包括前端的易用性优化措施,如泛型、主动装箱拆箱、条件编译等的内容的深入分析;以及后端的性能优化措施,如虚拟机的热点探测方法、HotSpot 的即时编译器、提前编译器,以及各种常见的编译期优化技术;
第五部分(第12~13章)高效并发
主要讲解了Java实现高并发的原理,包括Java的内存模型、线程与协程,以及线程安全和锁优化。
全书以实战为导向,通过大量与实际生产环境相结合的案例分析和展示了解决各种Java技术难题的方案和技巧。
《深入理解Java虚拟机》第3版的电子书已经打包好了
最后
秋招即来,金九银十即将到来。
大厂不断缩招,不容忽视的疫情影响,加上不断攀升的毕业生人数,各种需要应对的现实问题让整个求职季难上加难。
在这个异常残酷的求职季,很多人的困惑、面临的问题、面试求职的准备,真的需要一份过来人的经验贴。
看了很多网上的“八股文”,最后还是发现一个大佬整理的资料最全面、最干货、最有学习借鉴意义。
这份资料不仅包含了十多个互联网大厂的面试核心知识点、面经汇总和20个技术栈的资料合集,还分别有技术进阶、项目经验、面试突击不同版块的视频解析。
👇🏻 添加 博主 免费领取资料👇🏻 -
深入理解Java虚拟机.rar_JAVA虚拟机_java_深入理解JVM:pdf_深入理解Java_深入理解jvm
2022-07-15 19:45:05了解jvm的pdf,高清pdf,希望大家下载 -
java虚拟机源码-jvm-demo-code:深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)源代码
2021-05-21 01:23:12java虚拟机 源码 -
《深入理解java虚拟机》学习笔记
2022-01-12 00:21:29为了更加全面的理解java虚拟机,更好的对代码快进行理解,也为了更好的在面试中表述想法 java它的优点有(只列出两个): 一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题 实现了热点...前言
为了更加全面的理解java虚拟机,更好的对代码快进行理解,也为了更好的在面试中表述想法
java它的优点有(只列出两个):
- 一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题
- 实现了热点代码检测和运行时编译及优化,这使得Java应用能随着运行时间的增加而获得更高的性能
1. java内存区域与内存溢出异常
1.1 运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,
有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁
。
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
1.1.1 程序计数器
看作是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置
每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
1.1.2 java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)
也是线程私有的,它的生命周期与线程相同
。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame )用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中人栈到出栈的过程。-
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError异常;
-
如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError异常。
1.1.3 本地地方栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的
它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务
。1.1.4 堆
Java堆(Java Heap)是
Java虚拟机所管理的内存中最大的一块
。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例
,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(GarbageCollected Heap,幸好国内没翻译成“垃圾堆”)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,
存储的都仍然是对象实例
,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。根据Java虚拟机规范的规定,
Java堆可以处于物理上不连续的内存空间中
,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
1.1.5 方法区
方法区.(Method Area)与Java堆一样,
是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
1.1.6 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
1.2 OutOfMemoryError异常
除了程序计数器外,虚拟机内存的其他几个运行时区域都有可能发生oom异常
1.2.1 java堆溢出(不断创建对象)
Java堆用于存储对象实例,只要不断地创建对象
,并且保证GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。下面的代码展示:
- 限制Java堆的大小为20MB
- 不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展)
- 通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时
Dump 出当前的内存堆转储快照
以便事后进行分析。
要解决这个区域的异常,一般的手段是先通过
内存映像分析工具
对Dump出来的堆转储快照
进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory)-
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots 的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。
-
如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗
1.2.2 虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowEror 异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
2. 垃圾收集器与内存分配策略
先思考如下问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
目前动态分配与内存回收技术已经很成熟,进入自动化,至于为何还要了解GC和内存分配,这是因为要
排查各种内存溢出还是内存泄漏问题
介绍了运行时区域的各个部分,其中
程序计数器,虚拟机栈,本地方法栈
这三个区域随着线程的生而生,灭而灭。这几个区域的不需要过多的回收问题。方法结束或者线程结束,内存自然就回收了
。java堆和方法区不同,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样,这些只有在运行期间才知道创建哪些对象,内存的分配和回收都是动态的。
至于判断是否需要回收,通过如下:
在堆里面存放着Java中几乎所有的对象实例。
垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。
2.1 哪些内存需要回收
判断这种机制可以通过如下两种方法:
2.1.1 引用计数算法
给对象中
添加一个引用计数器
,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1﹔任何时刻计数器为0的对象就是不可能再被使用的。
但是,它有个缺点,就是很难解决对象之间相互循环引用的问题
举个简单的例子。
testGE()方法:对象obA和 objB都有字段instance赋值令objA.instance = objB及objB.instance = objA,除此之外,这两个对象再无任何何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。2.1.2 可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是
从GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
gcroot 引用的对象:- 虚拟机栈(栈帧中的本地变量表).中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
对于以前引用的这个定义如下:
在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用jdk1.2之后,多加了定义:
Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用
4种- 强引用:类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:描述一些还有用但并非必需的对象。对于软引用关联着的对象,
在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常
。在JDK 1.2之后,提供了SoftReference类来实现软引用。 - 弱引用:描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
。在 JDK 1.2之后,提供了WeakReference类来实现弱引用。 - 虚引用:最弱的一种引用关系。一个对象是否有虚引用的存在,
完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
2.2 垃圾收集算法
2.2.1 标记清除算法
为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
它的主要不足有两个:
一个是效率问题
,标记和清除两个过程的效率都不高;另一个是空间问题
,标记清除之后会产生大量不连续的内存碎片
,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
2.2.2 复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现
它将可用内存按容量划分为
大小相等的两块,每次只使用其中的一块
。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收
,内存分配时也就不用考虑内存碎片等复杂
情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。2.2.3 标记整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都f00%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记–清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
2.2.4 分代收集
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代
,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法
,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记一整理”算法来进行回收
。2.3 垃圾收集器
从Serial 收集器到 Parallel收集器,再到Concurrent Mark Sweep (CMS)乃至GC收集器的最前沿成果Garbage First (G1)收集器,我们看到了一个个越来越优秀(也越来越复杂)的收集器的出现,
用户线程的停顿时间在不断缩短
,但是仍然没有办法完全消除(这里暂不包括RTSJ中的收集器)。寻找更优秀的垃圾收集器的工作仍在继续!2.3.1 Serial收集器(单线程、客户端)
Serial收集器是最基本、发展历史最悠久的收集器
一个单线程的收集器
,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
。在用户不可见的情况下把用户正常工作的线程全部停掉
,这对很多应用来说都是难以接受的。读者不妨试想一下,要是你的计算机每运行一个小时就会暂停响应5分钟
单线程高效率,这点停顿对于客户端来说还是可以的,所以客户端中是一个不错的选择
2.3.2 ParNew收集器(多线程、服务端)
ParNew收集器其实就是Serial 收集器的
多线程版本
,除了使用多条线程进行垃圾收集之外,其余行为包括Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio,-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
ParNew收集器除了多线程收集之外,其他与Serial 收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器
,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
科普:CMS 收集器,真正意义上的并发(Concurrent)收集器,它第一次实现了让
垃圾收集线程与用户线程(基本上)同时工作
ParNew收集器在单CPU的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器
。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的
。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU 动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。2.3.3 Parallel Scavenge收集器(多线程、复制算法、吞吐量)
Parallel Scavenge收集器是一个
新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器
……看上去和ParNew都一样,那它有什么特别之处呢?Parallel Scavenge收集器的特点是它的关注点与其他收集器不同
CMS等收集器的关注点是尽可能地
缩短垃圾收集时用户线程的停顿时间
,而 Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
吞吐量和停顿时间是一个正比的效果,吞吐量多,停顿时间就会多。
2.3.4 Serial Old收集器(单线程、标记整理算法、客户端)
Serial Old是Serial 收集器的
老年代版本
,它同样是一个单线程收集器
,使用“标记一整理”
算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用
。如果在Server模式下,那么它主要还有两大用途:一种用途是在 JDK 1.5以及之前的版本中与ParallelScavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案
,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。2.3.5 Parallel Old收集器(多线程、标记整理算法)
Parallel Old是Parallel Scavenge收集器的
老年代版本,使用多线程和“标记一整理”算法
。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,
如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old (PS MarkSweep)收集器外别无选择
(还记得上面说过Parallel Scavenge收集器无法与CMS 收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew 加 CMS的组合“给力”。直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑
Parallel Scavenge加 ParallelOld收集器
。2.3.6 CMS收集器(标记清除)
CMS(Concurrent Mark Sweep)收集器是一种
以获取最短回收停顿时间
为目标的收集器。目前很大一部分的-Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。以上都有四个步骤,分别为初始标记,并发标记,重新标记,并发清除
其中,
初始标记、重新标记这两个步骤仍然需要“Stop The World"。
- 初始标记仅仅只是
标记一下GC.Roots能直接关联到的对象
,速度很快 - 并发标记阶段就是
进行GC Roots Tracing 的过程
- 重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生
变动的那一部分对象的标记记录
,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的
并发标记和并发清除过程收集器线程都可以与用户线程一起工作
,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。有以下明显几个缺点:
-
CMS 收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,
但是会因为占用了一部分线程或者说CPU资源)而导致应用程序变慢,总吞吐量会降低
。CMS 默认启动的回收线程数是(CPU 数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的 CPU资源,并且随着CPU 数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。为了应付这种情况,虚拟机提供了一种称为‘增量式并发收集器”(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,也就是速度下降没有那么明显
。实践证明,增量时的CMS收集器效果一般,不推荐使用 -
CMS收集器
无法处理浮动垃圾(Floating Garbage)
,可能出现“Concurrent ModeFailure”失败而导致另一次Full GC 的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
在 JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次"Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure"失败,性能反而降低。 -
会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
2.3.7 G1收集器(服务端)
优点有:
- 并行与并发:
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU
(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC 动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。 - 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记一清理”算法不同,G1从整体来看是基于“标记一整理”算法实现的收集器,从局部(两个Region之间)上来看是
基于“复制”算法实现的
,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。 - 可预测的停顿:这是G1相对于CMS 的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,
还能建立可预测的停顿时间模型
,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java (RTSJ)的垃圾收集器的特征了。
-
深入理解JAVA虚拟机.xmind
2020-06-26 11:15:13深入理解JAVA虚拟机.xmind -
深入Java虚拟机清晰版
2018-07-18 18:21:41作者以易于理解的方式深入揭示了Java虚拟机的内部工作原理,深入理解这些内容,将对读者更快速地编写更高效的程序大有裨益! 本书共分20章,第1-4章解释了Java虚拟机的体系结构,包括Java栈、堆、方法区、执行引擎... -
Java~阅读《深入理解Java虚拟机》知识点总结
2021-11-29 17:13:29文章目录前言第一部分(Java程序内部机制)1.1Java运行时一个类是什么时候被加载的?1.2JVM一个类的加载过程?1.3一个类被初始化的过程的注意点1.4继承时父子类的初始化顺序是怎样的?1.5究竟什么是类加载器?1.6JVM有... -
《深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版》-附件资源
2021-03-05 15:23:12《深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版》-附件资源 -
深入理解Java虚拟机 -- 经典垃圾收集器
2021-11-14 10:01:12本文参考于《深入理解Java虚拟机》 1. 综述 1. 总述: 如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、... -
深入理解java虚拟机-第三版-周明志 & Java虚拟机规范(java se 8) pdf
2021-08-31 14:34:56正所谓书中自有黄金屋,书中自有颜如玉。 关注我,一起来读书吧~ 关注我,回复:jvm 即可领取!!! 关注我,回复:jvm 即可领取!!! 关注我,回复:jvm 即可领取!!!