深入理解Java虚拟机
2023-05-09 10:55:20 19 举报
AI智能生成
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)思维导图
作者其他创作
大纲/内容
高效并发
Java内存模型与线程
Java内存模型
分支主题
内存间交互操作
·lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
·unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
·read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
·load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
·use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
·assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
·store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
·write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
原子性、可见性与有序性
原子性
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个,我们大致可以认为,基本数据类型的访问、读写都是具备原子性的
可见性
当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
有序性
如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-Serial
Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
先行发生原则
·程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
·管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
·volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。
·线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
·线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行
·线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
·对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
·传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
Java与线程
线程的实现
使用内核线程实现(1:1实现)
(“主流”平台上的“主流”商用Java虚拟机的线程模型普遍使用)
分支主题
使用用户线程实现(1:N实现)
分支主题
使用用户线程加轻量级进程混合实现(N:M实现)
(Solaris平台的HotSpot虚拟机)
分支主题
Java线程调度
协同式线程调度
线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去
实现简单、没有线程同步问题
线程执行时间不可控制,可能会导致整个系统崩溃
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
线程的执行时间是系统可控的,也不会有一个线程导致整个进程甚至整个系统阻塞的问题
状态转换
·新建(New):创建后尚未启动的线程处于这种状态。
·运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。
·无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。以下方法会让线程陷入无限期的等待状态:
■没有设置Timeout参数的Object::wait()方法;
■没有设置Timeout参数的Thread::join()方法;
■LockSupport::park()方法。
·限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
■Thread::sleep()方法;
■设置了Timeout参数的Object::wait()方法;
■设置了Timeout参数的Thread::join()方法;
■LockSupport::parkNanos()方法;
■LockSupport::parkUntil()方法。
·阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
·结束(Terminated):已终止线程的线程状态,线程已经结束执行。
分支主题
Java与协程
线程安全与锁优化
线程安全
Java语言中的线程安全
1.不可变
final关键字
2.绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施
可能需要付出非常高昂的,
甚至不切实际的代价
3.相对线程安全
保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性
在Java语言中,大部分声称线程安全的类都属于这种类型
4.线程兼容
对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用
Java类库API中大部分的类都是线程兼容的
5.线程对立
不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码
在java中通常都是有害的,应避免
线程安全的实现方法
1.互斥同步
synchronized关键字
Lock接口
2.非阻塞同步
CAS
ABA问题
版本号
3.无同步方案
如果能让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证其正确性
锁优化
自旋锁与自适应自旋
锁消除
锁粗化
轻量级锁
偏向锁
程序编译与代码优化
前端编译与优化
Javac编译器
Java语法糖
泛型、自动装拆箱、遍历循环、变长参数、条件编译
后端编译与优化
即时编译器
“客户端编译器”(Client Compiler)或者简称为C1编译器,简单优化,更高的编译速度
“服务端编译器”(Server Compiler),也叫C2编译器,高复杂度优化,更好编译质量
Graal编译器。JDK 10时才出现的、长期目标是代替C2的编译器
分层编译
第0层。程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。
·第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能。
·第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
·第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
·第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。
分支主题
提前编译器
编译器优化技术
方法内联
逃逸分析
栈上分配
标量替换
同步消除
公共子表达式消除
数组边界检查消除
自动内存管理
java内存区域与内存溢出异常
运行时数据区域
分支主题
程序计数器
可以看作是当前线程所执行的字节码的行号指示器。
在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机栈
线程私有的,生命周期与线程相同。
每个方法被执行的时候,Java虚拟机都
会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用相似,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
Java堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
(栈上分配、标量替换等优化手段已经导致对象实例都分配在堆上也变得不是那么绝对了)
方法区
是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
运行时常量池
是方法区的一部分。用于存放编译期生
成的各种字面量与符号引用
直接内存
不是虚拟机运行时数据区的一部分
NIO可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
HotSpot虚拟机对象
对象的创建
new指令->检查符号引用->执行类加载过程->分配内存->初始化零值->设置对象头->执行构造函数
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,在类加载检查通过后,接下来虚拟机将为新生对象分配内存。内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视
角看来,对象创建才刚刚开始——构造函数
new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
对象的内存布局
对象头(Header)
分支主题
实例数据(Instance Data)
是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容
对齐填充(Padding)
不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍
对象的访问定位
使用句柄
分支主题
使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
直接指针
分支主题
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访
问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本
HotSpot使用这种方式
垃圾收集器与内存分配策略
判断对象是否存活
引用计数算法
在对象中添加一个引用计数器,每当有一个地方
引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
循环引用问题:两个对象互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
java不使用这个算法
可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
Java、C#等主流语言使用
固定可作为GC Roots的对象
在虚拟机栈(栈帧中的本地变量表)中引用的对象
在方法区中类静态属性引用的对象
在方法区中常量引用的对象
在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器。
所有被同步锁(synchronized关键字)持有的对象
反映Java虚拟机内部情况的回调、本地代码缓存等
分支主题
java引用
强引用(Strongly Re-ference)
最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,如new
只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用(Soft Reference)
用来描述一些还有用,但非必须的对象。
只被软引用关联着的对象,在系统将要发生内
存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用(Weak Reference)
也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用(Phantom Reference)
是最弱的一种引用关系
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
要真正宣告一个对象死亡,至少要经历两次标记过程
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
垃圾收集算法
分代收集理论
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
标记-清除算法
最基础的垃圾收集算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
1.执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
2.内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
分支主题
标记-复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:将可用内存缩小为了原来的一半,空间浪费太多。
分支主题
现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代
新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。
优化(Appel式回收)
HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局
把新生代分为一块较大的Eden空间和两块较小的
Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。
当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(大多就是老年代)进行分配担保(Handle Promotion)。
如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代
标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
分支主题
Stop The World(STW)
移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行
完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。势必会直接影响应用程序的吞吐量
HotSpot虚拟机里面关注吞吐量的Parallel
Scavenge收集器是基于标记-整理算法的
关注延迟的CMS收集器则是基于标记-清除算法的,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。
HotSpot的算法细节实现
根节点枚举
安全点
安全区域
记忆集与卡表
写屏障
并发的可达性分析
经典垃圾收集器
分支主题
新生代
Serial收集器
是一个单线程工作的收集器,在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束,会存在Stop The World
客户端模式下的默认新生代收集器,简单而高效,它是所有收集器里额外内存消耗最小的
分支主题
ParNew收集器
实质上是Serial收集器的多线程并行版本
分支主题
除了Serial收集器外,目前只有它能与CMS
收集器配合工作,ParNew收集器是激活CMS后的默认新生代收集器,java9之前是官方推荐的服务端模式下的收集器解决方案
Parallel Scavenge收集器
是一款新生代收集器,同样是基于标记-复制算法,能够并行收集的多线程收集器(吞吐量优先收集器)
关注点在吞吐量,主要适合在后台运算而不需要太多交互的分析任务
老年代
Serial Old收集器
是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。
分支主题
Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现
在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
分支主题
CMS收集器
是一种以获取最短回收停顿时间为目标的收集器,是基于标记-清除算法实现
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。
初始标记仅仅只是标记一下GC
Roots能直接关联到的对象,速度很快;
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
优点:并发收集、低停顿
缺点:
对处理器资源非常敏感
因为占用了一部分线程(或者说处理器的计
算能力)而导致应用程序变慢,降低总吞吐量。
CMS默认启动的回收线程数是(处理器核心数量
+3)/4,当处理器核心数量不足四个时,
CMS对用户程序的影响就可能变得很大。可能导致用户程序的执行速度忽然大幅降低。
无法处理“浮动垃圾”
在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。
有可能出现并发失败进而导致另一次完全“Stop The World”的Full GC的产生
由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了
基于“标记-清除”算法,收集结束时会有大量空间碎片产生。
到内存空间的碎片化程度已经大到影响对象分配时,不得不提前触发一次Full GC以获得规整的内存空间。
分支主题
Garbage First收集器
主要面向服务端应用的垃圾收集器
在延迟可控的情况下获得尽可能高的吞吐
量
jdk9开始成为默认垃圾回收器
开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待
G1跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率
分支主题
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
初始标记:只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
·并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
·筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成
分支主题
低延迟垃圾收集器
Shenandoah收集器
OpenJDK才包含
JDK 12中加入
使用基于Region的堆内存布局,同样有着用于存放大对象的Humongous Region,默认的回收策略也同样是优先处理回收价值最大的Region
默认不使用分代收集,不会有专门的新生代Region或者老年代Region的存在
初始标记
并发标记
最终标记
并发清理
并发回收
初始引用更新
并发引用更新
最终引用更新
并发清理
初始标记:与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。
·并发标记:与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
·最终标记:与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集。最终标记阶段也会有一小段短暂的停顿。
·并发清理:这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region。
·并发回收:并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。并发回收阶段运行的时间长短取决于回收集的大小。
·初始引用更新:并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。
·并发引用更新:真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
·最终引用更新:解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
·并发清理:经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。
分支主题
ZGC收集器
JDK 11中加入
ZGC的Region可以具有大、中、小三类容量
小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置
4MB或以上的大对象。每个大型Region中只会存放一个大对象,大型Region在ZGC的实现中不会被重分配
分支主题
并发标记(Concurrent Mark)
并发预备重分配(Concurrent Prepare for Relocate)
并发重分配(Concurrent Relocate)
并发重映射(Concurrent Remap)
虚拟机执行子系统
类文件结构
虚拟机类加载机制
类加载的时机
类整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)
类加载的过程
加载
在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
解析
将常量池内的符号引用替换为直接引用
初始化
根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源
类加载器
类与类加载器
双亲委派模型
站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap
ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
站在Java开发人员的角度来看,自JDK 1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构
三层类加载器
启动类加载器(Bootstrap Class Loader):负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,Java虚拟机能够识别的类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用
扩展类加载器(Extension Class Loader):负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,开发者可以直接在程序中使用扩展类加载器来加载Class文件。
应用程序类加载器(Application Class Loader):负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
各种类加载器之间的层次关系被称为类加载器的双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
分支主题
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加
载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类
加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
破坏双亲委派模型
由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类
java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。
为了解决基础类型又要调用回用户的代码的问题,如JNDI、JDBC、JCE、JAXB和JBI等,引入了线程上下文类加载器,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性
原则
用户对程序动态性的追求而导致,如
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为
Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构
Java模块化系统
虚拟机字节码执行引擎
运行时栈帧结构
分支主题
Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine
Stack)的栈元素。
局部变量表
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
操作数栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种
字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。
动态连接
方法返回地址
附加信息
方法调用
解析
分派
动态类型语言支持
基于栈的字节码解释执行引擎
收藏
0 条评论
下一页