深入理解Java虚拟机:JVM高级特性与最佳实践(第三版)对此书进行学习并精简和加入自己理解

前世今生

内存模型

在这里插入图片描述

在这里插入图片描述
栈桢:一个方法对应一个栈桢,栈空间会自动释放

java虚拟机栈:与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

本地方法栈:与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。Native是java访问不到的地方,属于调用底层的C语言。(不是java来实现的方法体)Java在启动一个线程时,会调用start方法,start方法内部有一个start0方法,这个方法就进入本地方法栈,本地方法栈时用于管理本地方法的调用,而Java虚拟机栈是管理Java方法的调用。在本地方法栈(Native Method Stack)中,调用本地方法,通过本地方法接口(JNI)来实现对本地方法库的访问。

在这里插入图片描述
在这里插入图片描述

堆: 堆空间是new出的对象 无法自动释放对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。

方法区:所有线程共享,主要用于存储类的信息,常量池,方法数据,方法代码等。方法区逻辑上属于堆得一部分,但是为了和堆区分通常又叫非堆。方法区的实现,JDK1.7之前是永久代,JDK1.8之后是元空间。永久代在物理上是堆的一部分,元空间内存是操作系统本地内存。

在这里插入图片描述

静态常量池:其实叫“Class文件常量池”比较贴切,就是磁盘中class文件中Constant Pool列表。 可以使用javap命令查看class文件(如下图)会列出class文件中的常量池。

运行时常量池: 在内存中的常量池,既从Class文件加载后,将其常量数据放在内存中。具体而言是放在内存方法区Method Area中(Java7及以前为永久代,Java8及以后为MetaSpace元空间)

静态常量池与运行时常量池详细对比解释
推荐文章:静态常量池和运行常量池对比

字符串常量池:在Java 8,StringTable是存放在内存堆中

类的元数据是指描述类本身信息的数据。在Java中,每个类都有一些与其相关的元数据,这些数据包括但不限于以下内容:

类的名称和包路径: 元数据包括类的完全限定名称(包括包路径)。

父类和接口: 类继承的父类以及实现的接口信息。

字段信息: 包括类的各个字段的名称、类型和修饰符等。

方法信息: 类中定义的方法的名称、参数列表、返回类型和修饰符等。

常量池: 类中定义的常量和字符串等的信息,这些信息在运行时可以被使用。

访问控制信息: 类及其成员的访问权限修饰符,例如public、private、protected等。

注解信息: 类上使用的注解以及注解的属性信息。

字节码信息: 类的字节码指令,这些指令描述了类的具体实现,JVM可以根据字节码来执行类的方法。

在这里插入图片描述

对象的内存布局: 包含三个部分 对象头 实例数据 对齐填充

HotSpot 虚拟机的对象头包含两部分信息:
	第一部分:MarkWord用于存储对象自身的运行时数据如哈希码,gc代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳
	第二部分Klass Pointer:对象指向他的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个实例。
	
实例数据部分:对象真正存储的有效信息,父类和子类中定义的字段都必须记录起来。相同宽度的字段总是被分配到一起存放,在这个前提下父类中定义的变量会放在子类之前。

对象的第三部分是对齐填充,不是必然存在只是为了填充到八字节的倍数

对象的访问定位: java使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销(使用句柄访问会多一次开销)。
在这里插入图片描述
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访
问在Java中非常频繁,虚拟机HotSpot而言,它主要使用第二种方式进行对象访问。

OutOfMemoryError异常(OOM异常)情况

Java堆溢出(java.lang.OutOfMemoryError: Java heap space)

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
解决这个内存区域的异常,首先要通过内存映像分析工具对Dump出来的堆转存快照进行分析。
	第一步首先确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出。内存泄漏的情况下可以根据泄漏对象看到CG Roots的引用链,找的泄漏的对象是通过怎样的引用路径与哪些GCRoots关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
	如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗

内存泄漏:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC(Garbage Collection垃圾回收),这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

内存泄漏详细解释

虚拟机栈和本地方法栈溢出(栈容量只能由-Xss参数来设定)

1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常

方法区和运行时常量池溢出(·-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小)

运行时常量池是方法区的一部分,JDK8之后方法区由元空间实现很难产生方法区溢出了。因为元空间使用了操作系统本地内存不是JVM内存了

本机直接内存溢出(直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定)

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了

语言发展历史

C/C++
手动管理 malloc free/new delete
忘记释放
释放多次会产生机器难以调试的bug 一个线程空间莫名其妙被另一个释放了
java py go
方便的内存管理
GC 应用线程只负责分配,垃圾回收器负责回收(但是并没有解决野指针的问题)

垃圾

在这里插入图片描述

什么是垃圾:没有引用指向的内存空间就是垃圾
引用计数法(python使用): 计数有无引用没有引用则算为垃圾 但是会有循环引用问题 这时无法判定为垃圾
根可达算法(java使用): 从根上找找不到则被视为垃圾

垃圾收集器与内存分配策略

垃圾收集考虑三件事:哪些内存需要回收 什么时候回收 如何回收
垃圾收集器最关注的两个区域(java堆和方法区)的回收

确定哪些对象是活的还是死的

引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
在这里插入图片描述

引用计数法在对象头中添加了一个计数器来记录对象的被引用数。
引用计数法缺点无法解决循环引用的问题java不是使用引用计数法
应用计数法还有优化的方法详细的集中优化方法下面链接中展现
引用计数法详细讲解

可达性分析算法(java使用可达性分析算法)

在这里插入图片描述
可达性算法的思路通过一些列"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程中走过的路径成为引用链,如果对象到GC Roots间没有任何引用链相连,则证明此对象不能被再次利用

固定作为GC Roots的对象包括以下几种
1.验证虚拟机栈(栈帧中的局部变量)中引用的对象 作为GC Roots
(方法的局部变量) TestGCRoots01 t = new TestGCRoots01();
2.方法区中的类静态属性引用的对象
private static TestGCRoots02 t;
3.方法区中常量引用的对象
private static final TestGCRoots03 t = new TestGCRoots03(8 * _10MB);
4.本地方法栈中JNI(一般说的Native方法)中引用的对象
详细介绍参见此链接对几种GC root举例说明

再谈引用

在这里插入图片描述

强引用:强引用是使用最普通的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它例子Object o=new Object(); // 强引用,就算抛出OOM异常也不会进行回收。 如果超出生命周期范围gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候手机这要取决于gc的算法如果想要回收这个对象得令o=null弱化。
软引用(SoftReference):如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

String str=new String("abc");                                     // 强引用
 SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用  

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
这时候就可以使用软引用

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用        
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{
    prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev);       // 重新构建
}

弱引用(WeakReference):弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

String str=new String("abc");    
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
str=null;  

虚引用(PhantomReference): “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
可达性分析判定无法到达后一定处理么
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓
刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

垃圾收集算法

垃圾收集算法可分为两大类:引用计数式垃圾收集算法(Reference Counting Gc) 和 追踪式垃圾收集两大类。

分代收集理论

当前商业虚拟机的垃圾收集器大多数都遵循了"分代收集"的理论进行设计 此理论建立在两个假说的上面
1.弱分代假说:绝大多数对象都是朝生夕灭的
2.强分代假说:熬过多次垃圾收集过程的对象就越难以消亡

这两个原则决定了收集器应该将java堆划分出不同的区域,然后将回收对象依据其年龄分配出不同的区域。因为对内存空间分成了区域-所以才有了minor GC MajorGC FullGC 这三种回收类型-也才能针对不同的区域安排里面存储对象相对应的垃圾收集算法-因此发展了“标记-复制算法”“标记-清除算法”“标记整理算法”。

·部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单
独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收
集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

标记清除算法

可以通过标记所有可达节点,然后清除所有未标记节点。反过来也可以
缺点:
1.执行效率不稳定,如果大面积回收,标记和清除两个工作大量增加。
2.内存空间碎片化问题,标记清除之后会产生大量不连续的碎片,空间碎片太多会导致之后有大对象没有空间,而触发又一次垃圾收集
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d6fee69f12cc4dd28d7c53bd663500d9.png#pic_center

标记复制算法

在这里插入图片描述
首先做一次标记(在FROM区域),就找到那些不被根对象引用的对象标记为垃圾,然后在FROM区域这些存活的对象内存空间复制到TO区域中,复制的过程中就会完成碎片的整理,因此也不会产生碎片,等复制完成后,可以看到FROM区域全是垃圾,所以一下子给予清空,并且之后交换FROM和TO他两的位置,即原来的TO变成了FROM,原来的FROM变成了TO,所以TO总是空闲的一块儿。

优点:没有内存碎片
缺点:需要双倍的内存空间

标记整理算法

在这里插入图片描述
和标记清除一样,标记整理的第一个阶段也是对垃圾对象进行标记,区别主要在第二个步骤,即整理。所谓的整理就是避免之前标记清除时的内存碎片的问题,他就会在清除的过程中,会把可用的对象向前给他移动,这样的话让内存更为紧凑,这就是整理的过程。整理之后,就能发现内存变的更紧凑了,即连续的空间就更多了,这样就不会造成内存碎片。
优点:
整理之后,就能发现内存变的更紧凑了,即连续的空间就更多了,这样就不会造成内存碎片。
解决了“标记-复制算法”需要分配担保的问题。
缺点:
1、“标记-清除算法”在分配对象阶段更为复杂,“标记-整理算法”是移动式回收算法,在老年代中大部分对象都是存活的,因此在回收对象时,会伴随大量的对象移动,从而会导致对象回收阶段会占用相对多的时间
2.如果那些局部变量或对象引用了这个,那么还得改变那些变量或对象的引用地址,因为内存地址变了,所以涉及的工作就比较多一些,牵扯到内存区块的拷贝移动,牵扯到所有引用的地址加以改变。所以速度较慢

并发性可达分析

并发的可达性分析要借助三色标记法,程序要一边运行一边进行标记
·白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
·黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有成员对象都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

·灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个成员对象还没有被扫描过
三色标记法视频解析
用户线程与收集器是并发工作可能产生的问题

1.把原本消亡的对象错误标记为存活,影响不大
2.把原本存活的对象标记为消亡,程序会产生错误

用图来解释
在这里插入图片描述
Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问
题,即原本应该是黑色的对象被误标为白色:
·赋值器插入了一条或多条从黑色对象到白色对象的新引用;
·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

若何解决这个问题;从以下两个任意选一个就行

1.增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
2.原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

经典垃圾收集器

不同垃圾收集器的产生总体可以划分为几个阶段。。

第一阶段:单线程收集时代(Serial和Serial Old)
第二阶段:多线程收集时代(Parallel Scanvenge 和Parallel Old)
第三阶段:并发收集时代(ParNew和CMS)
第四阶段:通用并发收集时代(G1)

在这里插入图片描述
如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

Serial收集器(新生代收集器)

1.单线程工作的收集器(进行垃圾收集时必须暂停其他所有工作线程,直到它收集结束)
在这里插入图片描述
它是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着简单高效的优点Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好地选择

ParNew收集器(新生代收集器)

ParNew收集器实质上是Serial收集器的多线程并行版本,
在这里插入图片描述
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge收集器(新生代收集器)

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
在这里插入图片描述
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
-XX:MaxGCPauseMillis参数:收集器要尽力保证内存时间不超过设定值,缩短这种时间的代价是牺牲吞吐量和新生代空间为待见换取的。新生代空间减小会导致GC更频繁每次停顿秒数会减少吞吐量会下降

例子:收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

-XX:GCTimieRatio参数的值则是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率相当于吞吐量的倒数。

-XX +UseAdaptiveSizePolicy参数 这个参数开启后吞吐量大小会进行动态调节,自动调节新生代老年代,提供最合适的吞吐量。

Serial Old收集器(老年代收集器)

在这里插入图片描述
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器(老年代收集器)

到JDK6之前了Parallel Scavenge收集器只能配合Serial Old收集器进行使用直到。Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
在这里插入图片描述

CMS收集器

STW: Stop-The-World: 是在垃圾回收算法执⾏过程当中,将JVM内存冻结丶应用程序停顿的⼀种状态。
CMS 的全称是 Mostly Concurrent Mark and Sweep Garbage Collector(主要并发­标记­清除­垃圾收集器),它在年轻代使用复制算法,而对老年代使用标记-清除算法CMS 的设计目标,是避免在老年代 GC 时出现长时间的卡顿(但它并不是一个老年代回收器)。如果不希望有长时间的停顿,同时你的 CPU 资源也比较丰富,使用 CMS 是比较合适的
CMS 使用的是 Sweep 而不是 Compact,所以它的主要问题是碎片化。随着 JVM 的长时间运行,碎片化会越来越严重,只有通过 Full GC 才能完成整理。不使用整理算法的原因是CMS的目的就是为了减少卡顿但整理内存的时刻可能会导致STW
在这里插入图片描述

CMS收集器是基于标记-清除算法实现优点是使停顿时间尽可能短。整个过程分为四个步骤。
1)初始标记CMS 2)并发标记3)重新标记4)并发清除

初始化标记

初始化标记阶段,只标记直接关联的GC root的对象,不用向下追溯。因为最耗时的就在tracing阶段,这样就极大缩短了初始标记时间。这个过程是 STW 的,但由于只是标记第一层(GC Root),所以速度是很快的。

并发标记 对老年代所有对象进行GC Roots追踪(最耗时)

在初始标记的基础上,进行并发标记。这一步骤主要是 tracinng 的过程,用于标记所有可达的对象。这个过程会持续比较长的时间,但却可以和用户线程并行。在这个阶段的执行过程中,可能会产生很多变化:

有些对象,从新生代晋升到了老年代;
有些对象,直接分配到了老年代;
老年代或者新生代的对象引用发生了变化。

重新标记阶段 (STW)

为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始阶段稍长一些,但也远比并发标记阶段的时间短。(详情要参考三色标记法)

并发清除阶段

清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计
算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量
+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。
由于并发标记和并发清理阶段:用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生CMS无法在当次收集中处理掉它们,这一部分垃圾被称为浮动垃圾。浮动垃圾指的是那些本应该被清理的却被标上了颜色(再一次GC才能解决)。

JDK 5 之后默认为百分之62老年代被使用会激活GC
在这里插入图片描述

CMS优缺点分析总结
优缺点总结

优势:低延迟,大部分垃圾回收过程并发执行
劣势1.内存碎片,FULLGC的整理阶段会有较长停顿2.需要预留空间,用来分配阶段产生"浮动垃圾"3.使用更多的CPU资源,在应用运行的同时进行堆扫描。4.CMS 对老年代回收的时候,并没有内存的整理阶段。这就造成程序在长时间运行之后,碎片太多。如果你申请一个稍大的对象,就会引起分配失败。
CMS 提供了两个参数来解决内存碎片啊问题:
UseCMSCompactAtFullCollection(默认开启),表示在要进行 Full GC 的时候,进行内存碎片整理。内存整理的过程是无法并发的,所以停顿时间会变长。
CMSFullGCsBeforeCompaction,每隔多少次不压缩的 Full GC 后,执行一次带压缩的 Full GC。默认值为 0,表示每次进入 Full GC 时都进行碎片整理。

所以,预留空间加上内存的碎片,使用 CMS 垃圾回收器的老年代,留给我们的空间就不是太多,这也是 CMS 的一个弱点。

G1垃圾收集器

G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU以及大容量内存的机器,以及极高的概率满足GC停顿时间的同事还兼具了高吞吐量的心梗特征。是JDK 9以后的默认垃圾回收器,取代了CMS回收器以及Parallel + Parallel old组合。被oracle官方称为“全功能的垃圾收集器。
G1的目标在延迟可控的情况下获得尽可能高的吞吐量

G1的工作原理

G1是一个并行回收器他把堆内存分割为很多不相关的区域(物理上不连续)
使用不同的Region来表示Eden
1.幸存者0区 2.幸存者1区 老年代等
G1会有计划地避免进行全区域垃圾收集
根据每个Region垃圾堆价值大小来进行回收,后台维护一个优先列表每次根据允许的收集时间优先回收价值大的Region。价值由所获得的空间大小,以及回收所需要时间的经验值来判定。
由于这种方式的侧重点在于回收垃圾最大量的区间,垃圾优先(Garbage First) 。
G1中提供了三种垃圾回收模式: YoungGC、Mixed GC和Full GC,在不同的条件下被触发。

G1的特点

G1使用了全新的分区算法

并行与并发
并行性: G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW(之前多是一个GC和多个用户)
并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况

分代收集
从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代和之前的各类回收器不同,它	同时兼顾年轻代和老年代。其他回收器要么工作在年轻代,要么工作在老年代; 

在这里插入图片描述

HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程

G1的优缺点

优点:由于Region采用了复制算法,但整体上实际可以看做压缩标记算法。这两种算法可以避免内存碎片,有利于程序长时间运行,分配对象时不会因为没有大空间GC。后台维护一个优先列表,每次收集时会回收价值最大的分区保证了G1收集器在有限时间内高效率进行收集。
在下面的情况时,使用G1可能比CMS好:

超过50%的Java堆被活动数据占用
对象分配频率或年代提升频率变化很大
GC停顿时间过长(长于0.5至1秒)。
分区region——化整为零

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,**且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。 如果设置了Region数量,那么Region大小就不是固定的,但是大小肯定是2的幂次方,**并且在1~32M之间;如果设置了Region大小,那么Region数量就不是固定的,但是肯定是2048附近。region块的大小可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。

1)线程本地分配缓冲区 Thread Local allocation buffer (TLab):
如果对象在一个共享的空间中分配,那么我们就需要采用同步机制来解决并发冲突问题,而为了减少并发冲突损耗的同步时间,G1 为每个应用线程和GC线程分配了一个本地分配缓冲区TLAB,分配对象内存时,就在这个 buffer 内分配,线程之间不再需要进行任何的同步,提高GC效率。但是当线程耗尽了自己的Buffer之后,需要申请新的Buffer。这个时候依然会带来并发的问题。G1回收器采用的是CAS(Compate And Swap)操作。

    显然的,采用TLAB的技术,就会带来碎片。举例来说,当一个线程在自己的Buffer里面分配的时候,虽然Buffer里面还有剩余的空间,但是却因为分配的对象过大以至于这些空闲空间无法容纳,此时线程只能去申请新的Buffer,而原来的Buffer中的空闲空间就被浪费了。Buffer的大小和线程数量都会影响这些碎片的多寡。

Region只能是Eden、Survivor、Humongous中的一种,但是它的身份不是固定的,谁来占用那么这个Region就是谁的。一个region 有可能属于Eden,Survivor或者 Old/Tenured 内存区域。但是一个region只可能属于一个角色。3.3图中的E表示该region属于Eden内存区域,s表示属于survivor内存区域,o表示属于old内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做 Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个region,就放到H。
设置H的原因:对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。

主要环节

在这里插入图片描述
初始标记: 仅仅标记一下GC roots能直接关联的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿

并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录

筛选回收:负责更新Region的统计数据,对各个Region的回收价值成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收**的那一部分Region的存活对象复制到空的Region中,**再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的

G1相比CMS优点 可以指定最大停顿时间,可以分Region内存布局,按受益动态确定回收这些内存
G1整体上基于标记整理泛局部基于标记赋值算法,所以G1不会有内存碎片长期有利。

G1 垃圾回收器会对每个分区里的垃圾对象进行统计,计算出每个分区的回收价值,即回收该分区所能获得的空间和所需时间的比值12。
G1 垃圾回收器会根据用户设置的停顿时间目标,动态地选择回收价值最高的分区进行回收,这些分区称为回收集12。
G1 垃圾回收器会在后台维护一个优先级列表,按照回收价值从高到低排序,每次回收时从列表中选择合适的分区加入回收集12。
G1 垃圾回收器还会考虑分区之间的引用关系,尽量选择引用较少的分区进行回收,以减少并发标记的开销12。

缺点 G1相比CMS卡表更加复杂。占堆容量更多。

三种垃圾回收方式

新生代回收(YGC = minor GC):只回收新生代区域,代价低频率高
混合回收(MixGC):回收全部新生代+不部分老年代 频率一般(老年代占据堆空间超过百分之45就会触发)
完全回收(FULL GC):全部堆空间,代价高频率低距离崩溃不远了

在这里插入图片描述
G1对象管理过程:
在这里插入图片描述
在这里插入图片描述

新生代回收详解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果超过堆空间占用超过百分之45了需要开启混和GC 因此需要判断是否需要开启并发标记

在这里插入图片描述
一旦发现活着的对象就进行复制,而不是标记全结束了才开始

混合回收和FULL回收都是同事处理新生代和老年代区域

对象什么时候进入老年代

1.Eden区如果对象经过几轮YGC仍然存活或者触发了动态年龄判断规则,会进入老年代
2.趸货对象S区已经放不下了,会让对象进入老年代
3.大对象直接进入单独二大对象Region,但是大对象区域本身占用的就是老年代区间

混合回收什么时候发生

在YGC之后已经分配的内存总容量超过45%会触发参数是-xx:InitiatingHeapOccupancyPercent
混合回收基本步骤
第一步:初始标记阶段标记出所有由GCRoot等直接引用的对象,会暂停用户程序运行
第二步:并发标记阶段 标记处上一步中所有引用的对象,执行时间略微长。但是用户程序也会同时执行不会STW。
第三步:再标记阶段标记出上一个阶段没有标记的对象会STW,执行速度会非常快。由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。原因:并发标记不准确。
第四步:存活对象技术阶段统计出每个Region存活对象的数量,为什么要统计呢?下一步回收时只会从老年代回收一部分区域,因此先统计每个区域存活数量,垃圾对象以及占比高低,才能判断该如何选择才能满足用户设定的停顿时间并保证收益最大。
第五步:垃圾回收阶段选择回收价值高的区域,把存活对象复制到新分区,然后回收掉老区域

在这里插入图片描述
YGC是MixedGC的前奏,YGC完成就代表mixedGC已经走完了初始标记阶段,YGC已经帮MixedGC干完了初始化的活。MixedGC之前一定要先进行一次YGC。
所以Mixed之前一定要进行一次YGC。
混合回收为啥要多次进行
某个Region有百分之85为存活对象则不再进行回收。
比如一次需要回收400个Region但是停顿时间一次只能50个,所以需要停顿多次来进行回收通过XX:G1 MixedGCountTarget设置次数来进行多次回收

FULLGC

触发条件YGC和MixedGC都不够分配对象触发Full GC, 永久区满了也会触发Full GC
FULLGC可能进行两次 第二次是回收软引用如果仍然不够使用那么证明对象无法分配要进行OOM了

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

垃圾收集器与内存分配策略

基础故障处理工具

jps:虚拟机进程状况工具

JPS输出进程信息可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。虽然功能比较单一,但它绝对是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID,Process Identifier)是一致的,使用Windows的任务管理器或者UNIX的ps命令也可以查询到虚拟机进程的LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就必须依赖jps命令显示主类的功能才能区分了。
在这里插入图片描述

jstat:虚拟机统计信息监视工具

jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程[1]虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据
命令: jstat [ option vmid [interval[s|ms] [count]] ]
选项option代表用户希望查询的虚拟机信息,主要分为三类:类加载、垃圾收集、运行期编译状况。
在这里插入图片描述

jinfo:Java配置信息工具

jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。使用jps命令的-v参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用jinfo的-flag选项进行查询了

jmap:Java内存映像工具

jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不使用jmap命令,要想获取Java堆转储快照也还有一些比较“暴力”的手段:譬如在第2章中用过的-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。

jmap命令格式: jmap [ option ] vmid

jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等

在这里插入图片描述

jstack:Java堆栈跟踪工具

jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack命令格式:jstack [ option ] vmid
在这里插入图片描述

可视化故障管理工具

JConsole:Java监视与管理控制台

JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整JMX是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以运行于虚拟机之上的软件中,典型的如中间件大多也基于JMX来实现管理与监控虚拟机对JMXMBean的访问也是完全开放的,可以使用代码调用API、支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问。

1.启动JConsole

通过JDK/bin目录下的jconsole.exe启动JCon-sole后,会自动搜索出本机运行的所有虚拟机进程,而且不需要自己使用JPS来查询了。

2.内存监控

“内存”页签的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存(被收集器直接管理的Java堆和被间接管理的方法区)的变化趋势。

3.线程监控

如果说JConsole的“内存”页签相当于可视化的jstat命令的话,那“线程”页签的功能就相当于可视化的jstack命令了,遇到线程停顿的时候可以使用这个页签的功能进行分析。

类文件结构

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

无关性的基石

各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式——字节码是构成平台无关性的基石
java虚拟机两个无关性平台无关性(一次编写到处运行)和语言无关性(其他语言可以在java虚拟机上运行)

平台无关性基础程序编译后是字节码文件运行到虚拟机上运行,不同平台运行虚拟机虚拟机来运行字节码文件就可以

实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。(语言无关性图示)
在这里插入图片描述

Class类文件的结构

Class的文件结构非常稳定,多年来Class文件格式进行了几次更新但都是在原有结构上新增内容,扩充功能,没有对已经定义的内容进行修改。

Class文件以8个字节为基础的二进制流,各个数据项目按照严格顺序紧凑排列,中间几乎没有分隔符。所以Class文件中存储的内容几乎全是程序必须运行的。

Clalss文件采用类似C语言结构体的伪结构体来存储数据只有两种数据类型‘无符号数’和表

为了区分两种数据类型表会用_info进行结尾

·无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

Class文件结构展示(如图)
在这里插入图片描述

魔数与class版本号

魔数:文件开头4个字节称之为魔数唯一作用确定该Class文件是否被虚拟机接受
版本号:紧接着魔数的4个字节,7,8字节版本号是主版本号,5,6字节是此版本号
主版本号对应不同版本JDK,高版本JDK能运行低版本号Class文件但是反过来不行

常量池

紧接着版本号的是常量池入口。
Class文件中第一个出现的表类型数据目录。
常量池入口会有一个 常量池容量计数值(count)用于统计常量池中常量的数量
常量池的两大常量
1.字面量
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
2.符号引用
属于编译原理方面的概念主要包括下面几类常量:
被模块导出或者开放的包(Package):这是Java 9引入的模块系统(Module System)的特性,用于控制包的可见性和依赖性。对应的代码的程序是module-info.class文件,它定义了模块的名称、导出的包、开放的包、依赖的模块等信息。
类和接口的全限定名(Fully Qualified Name):这是指包含了包名和类名的完整的类或接口的名称,例如java.lang.String4。对应的代码的程序是任何定义或引用了类或接口的Java源文件,例如

package com.example;
import java.util.List;
public class Foo {
    private List<String> names;
    // java.util.List是接口的全限定名,表示在java.util包下定义了一个名为List的接口
}

字段的名称和描述符(Descriptor):这是指类或接口中定义的变量的名称和类型,例如private int x的名称是x,描述符是I。对应的代码的程序是任何定义或引用了字段的Java源文件,例如

package com.example;
public class Bar {
    public static final int MAX_VALUE = 100;
    //MAX_VALUE是字段 描述符是I
    private Bar bar;
    // 字段是bar描述符是Lcom/example/Bar;
}

方法的名称和描述符:这是指类或接口中定义的函数的名称和参数类型、返回类型,例如public void foo(int x, String y)的名称是foo,描述符是(ILjava/lang/String;)V。对应的代码的程序是任何定义或引用了方法的Java源文件,例如:

package com.example;
public class Baz {
    public Baz() {
    }
    public int add(int a, int b) {
        return a + b;
    }
    // public Baz()是构造方法(constructor)的方法名,描述符是()V,表示没有参数(parameter)和返回值(return value)的方法。
    //public int add(int a, int b)是普通方法(regular method)的方法名,描述符是(II)I,表示有两个整型(int)的参数和一个整型的返回值的方法。
}

方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic):这是Java 7引入的用于支持动态语言的特性,用于表示方法的引用和类型,以及动态调用方法的指令。对应的代码的程序是使用了java.lang.invoke包中的类和方法的Java源文件,例如:

package com.example;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class Quux {
    public static void hello(String name) {
        System.out.println("Hello, " + name);
    }
    public static void main(String[] args) throws Throwable {
        MethodType mt = MethodType.methodType(void.class, String.class); 
        //方法类型这表示一个没有返回值(void)的方法,它接受一个字符串(String)类型的参数
        MethodHandle mh = MethodHandles.lookup().findStatic(Quux.class, "hello", mt);
        //方法句柄是一个可以像方法一样被调用的对象,它可以用MethodHandles.lookup方法查找,例如
        mh.invokeExact("world");
        //这表示一个静态方法(static method)的句柄,它属于Quux类,名为hello,并且具有方法类型mt。方法句柄可以用invoke或invokeExact方法调用
    }
}

动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接

常量池中每一项常量都是一个表最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量,为了支持Java模块化系统(Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK13,常量表中分别有17种不同类型的常量
17种常量类型
在这里插入图片描述

访问标志

常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final(简而言之就是类名前的东西由访问标志进行掌控)
在这里插入图片描述

类索引、父类索引与接口索引集合

紧接着访问标志之后都是u2类型,类索引 用于确定这个类的全限定名,父索引用于确定这个类的父类全限定名。Java语言不存在多继承,除了Object类之外都有父类且只有一个父类。接口索引集合用于描述这个类实现了哪些接口

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)。
各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标
志位来表示

字段数据类型(基本类型、对象、数组)、字段名称。
Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量
字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

方法表集合

Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)(对应常量池)、属性表集合(attributes)几项

JVM中class结构里常量池符号引用是指用符号来表示类、字段或方法的引用,而不是具体的内存地址。符号引用可以是类或接口的全限定名、变量或方法的名称、变量或方法的描述信息等1。符号引用在编译时生成,但在运行时才被解析为直接引用,即实际的内存地址2。

class结构中的字段表和方法表是用来存储类的属性和方法的信息的。字段表和方法表中的每个项都包含一个指向常量池的索引,用来表示字段或方法的名称和描述符

字节码指令简介

JAVA虚拟机指令: 操作码+操作数 成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构(这两种架构的执行过程、区别和影响将在第8章中探讨),所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。

机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不能够超过256条

字节码与数据类型

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为
哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈(见第2章关于内存区域的介绍)之
间来回传输,这类指令包括:
·将一个局部变量加载到操作栈:iload、iload_、lload、lload_、fload、fload_、dload、
dload_、aload、aload_
·将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、lstore_、fstore、
fstore_、dstore、dstore_、astore、astore_
·将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、
iconst_、lconst_、fconst_、dconst_
·扩充局部变量表的访问索引的指令:wide

运算指令

·加法指令:iadd、ladd、fadd、dadd
·减法指令:isub、lsub、fsub、dsub
·乘法指令:imul、lmul、fmul、dmul
·除法指令:idiv、ldiv、fdiv、ddiv
·求余指令:irem、lrem、frem、drem
·取反指令:ineg、lneg、fneg、dneg
·位移指令:ishl、ishr、iushr、lshl、lshr、lushr
·按位或指令:ior、lor
·按位与指令:iand、land
·按位异或指令:ixor、lxor
·局部变量自增指令:iinc
·比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
类型转换指令可以将两种不同的数值类型相互转换,这些转换操作一般用于实现用户代码中的显
式类型转换操作,或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型
一一对应的问题。
Java虚拟机直接支持(即转换时无须显式的转换指令)以下数值类型的宽化类型转换(Widening
Numeric Conversion,即小范围类型向大范围类型的安全转换):
·int类型到long、float或者double类型
·long类型到float、double类型
·float类型到double类型
与之相对的,处理窄化类型转换(Narrowing Numeric Conversion)时,就必须显式地使用转换指
令来完成,这些转换指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。

对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指
令(在下一章会讲到数组和普通类的类型创建过程是不同的)。对象创建后,就可以通过对象访问指
令获取对象实例或者数组实例中的字段或者数组元素,这些指令包括:
·创建类实例的指令:new
·创建数组的指令:newarray、anewarray、multianewarray
·访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的
指令:getfield、putfield、getstatic、putstatic
·把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、
daload、aaload
·将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、
dastore、aastore
·取数组长度的指令:arraylength
·检查类实例类型的指令:instanceof、checkcast

操作数栈管理指令

将操作数栈的栈顶一个或两个元素出栈:pop、pop2
·复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、
dup2_x1、dup_x2、dup2_x2·将栈最顶端的两个数值互换:swap

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

虚拟机类加载机制

第一,在实际情况中,每个Class文件都有代表着Java语言中的一个类或接口的可能,后文中直接对“类型”的描述都同时蕴含着类和接口的可能性,而需要对类和接口分开描述的场景,笔者会特别指明;
第二,与前面介绍Class文件格式时的约定一致,本章所提到的“Class文件”也并非特指某个存在于
具体磁盘中的文件,而应当是一串二进制字节流,无论其以何种形式存在,包括但不限于磁盘文件、网络、数据库、内存或者动态产生等

JAVA虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制。

类加载的时机

在这里插入图片描述
解析阶段则解析阶段可以在初始化后进行
按部就班地“开始”,而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
·使用new关键字实例化对象的时候。
·读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
·调用一个类型的静态方法的时候
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

被动引用不会触发初始化

类加载的过程

加载

加载时整个类加载过程的一个阶段。
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证

文件格式验证

·是否以魔数0xCAFEBABE开头。
·主、次版本号是否在当前Java虚拟机接受范围之内。
·常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
·指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
·CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
·Class文件中各个部分及文件本身是否有被删除的或附加的其他信息

元数据验证

在这里插入图片描述

字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定
程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析
,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:
·保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作
栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。
·保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
·保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全
的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。本阶段通常需要校验下列内容:
·符号引用中通过字符串描述的全限定名是否能找到对应的类。
·在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
·符号引用中的类、字段、方法的可访问性
(private、protected、public、)是否可被当前类访问。

准备

在这里插入图片描述

解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
在这里插入图片描述
方法的直接引用对应对应方法表 其他各种直接引用对应各种对应表 实例变量的直接引用对应内存位置

初始化

直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程
序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通
过程序编码制定的主观计划去初始化类变量和其他资源。

我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器<Client ()方法的过程。<Client ()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。
·接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
()方法。但接口与类不同的是,执行接口的<Client ()方法不需要先执行父接口的<Client ()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的<Client ()方法
·<Client ()方法与类的构造函数(即在虚拟机视角中的实例构造器<init()方法)不同,它不需要显
式地调用父类构造器,Java虚拟机会保证在子类的<Client ()方法执行前,父类的<Client ()方法已经执行完毕
在这里插入图片描述
静态代码块和静态变量部分在初始化阶段进行初始化。
非静态部分由构造函数方法,产生实例的时候调用构造函数时刻进行初始化。

类加载器

类与类加载器

类本身+类加载器共同确立在JAVA虚拟机中的唯一性
两个类源于同一个Class文件,被同一个java虚拟机加载,只要类加载器不同那么这两个类必定不同

双亲委派模型

JAVA虚拟机认为只存在两种类加载器

1.启动类加载器(Bootstrap)c++实现是虚拟机自身的一部分
2.其他类加载器java语言实现,独立存在于虚拟机外部,并且全部继承抽象类java.lang ClassLoader

目前java保持着三层类加载器,双亲委派的类加载架构。
三层类加载器
1.·启动类加载器(Bootstrap Class Loader):前面已经介绍过,这个类加载器负责加载存放在JAVA_HOME\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类
库加载到虚拟机的内存中。
(1)使用c/c++实现嵌套在jvm内部
(2)加载java核心库

JAVA_HOME/jre/lib/rt.jar
sun.boot.class.path路径下的内容
(3)不继承ClassLoader,没有父类加载器
(4)只加载包名为java,javax,sun开头的类
(5)加载扩展类加载器和应用程序类加载器

2.扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件

(1)java语言,sun.misc.Launcher.$ExtClassLoader实现
(2)继承于ClassLoader类
(3)父类加载器为启动类加载器
(4)加载java.ext.dirs/jre/lib/ext指定的目录

3.应用程序类加载器(Application Class Loader):这个类加载器由
sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

在这里插入图片描述双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

(1)java语言,sun.misc.Launcher.$AppClassLoader实现
(2)继承于ClassLoader类
(3)父类加载器为扩展类加载器
(4)加载环境变量classpath或者系统属性java.class.path指定路径下的类
(5)用户自定义类加载器的默认父加载器
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

破坏双亲委派模型

类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,JDK1.2java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。上节我们已经分析过loadClass()方法,双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。
由上面语句可以知道两点

1.更改LoadClass可以破坏双亲委派
2.重写finclass 可以不破坏双亲委派写自己加载逻辑

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码。解决这个问题JNDI服务破坏了双亲委派用了父类加载器去请求子类加载器。

第三次破坏双亲委派是由于用户对程序动态性追求引起的,如设备热插拔

虚拟机字节码执行引擎

运行时栈结构

JAVA虚拟机最基本的执行单元是方法,栈桢则是用于支撑虚拟机执行方法和调用的数据结构,是虚拟机栈中的元素。
栈桢包括了局部变量表,操作数栈,动态链接,方法返回地址+附加信息
栈中只有栈桢顶的栈桢才生效,称之为当前栈桢被当前栈桢关联的方法称之为当前方法
执行引擎所有的字节码指令都是只针对当前栈桢进行操作的

局部变量表

用于存放方法参数和内部定义的局部变量
在java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了改方法所需分配的局部变量表的最大容量
局变量的容量用变量槽为基本单位一个变量槽可以存一个32位的数据
在这里插入图片描述
变量槽中可以存储的数据
有boolean、byte、char、short、int、float、reference,returnAddress

reference介绍
一是从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引,
二是根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
类中的变量会在准备阶段赋值 但是方法中的局部变量必须初始化

操作数栈

在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了。
在这里插入图片描述

动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。通过第6章的讲解,我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数
这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。
另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

方法返回地址

返回有两种情况
1.一种退出方式是在方法执行的过程中遇到了异常
2.一种是遇到了返回指令

方法调用

方法调用的唯一任务就是确定调用的是哪一个方法。
一切方法调用在Class文件里存储的都只是符号引用,而不是方法在实际运行时内存布局的入口地址

解析

invokestatic。用于调用静态方法。
·invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法。
·invokevirtual。用于调用所有的虚方法。
·invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。
·invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4
条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-Virtual Method),与之相反,其他方法就被称为“虚方法”(Virtual Method)
(称之为解析方法)

分派

静态分派

静态分派是指在编译时就确定了被调用的方法的版本 主要发生在重载中
public class StaticDispatch {
	static abstract class Human {
	}
	static class Man extends Human {
	}
	static class Woman extends Human {
	}
	public void sayHello(Human guy) {
	System.out.println("hello,guy!");
	}
	public void sayHello(Man guy) {
	System.out.println("hello,gentleman!");
	}
	public void sayHello(Woman guy) {
	System.out.println("hello,lady!");
	}
	public static void main(String[] args) {
		Human man = new Man();
		Human woman = new Woman();
		Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
		StaticDispatch sr = new StaticDispatch();
		sr.sayHello(man);
		sr.sayHello(woman);
		sr.sayHello((Man) human)
		sr.sayHello((Woman) human)
	}//运行结果
hello,guy!
hello,guy!
hello,gentleman!
hello,lady!

静态分派编译时刻就确定了所以调用方法根据引用就确定定了 而new是运行时刻才能确定所以根据引用才确定了

动态分派

态分派是指在运行时根据对象的实际类型来确定被调用的方法的版本。 主要发生在重写中。

.单分派与多分派

宗量定义 : 方法的接收者与方法的参数统称为方法的宗量

/**
* 单分派、多分派演示
* @author zzm
*/
public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
	public void hardChoice(QQ arg) {
		System.out.println("father choose qq");
	}
	public void hardChoice(_360 arg) {
		System.out.println("father choose 360");
	}
}
public static class Son extends Father {
	public void hardChoice(QQ arg) {
		System.out.println("son choose qq");
	}
	public void hardChoice(_360 arg) {
		System.out.println("son choose 360");
	}
}
	public static void main(String[] args) {
		Father father = new Father();
		Father son = new Son();
		father.hardChoice(new _360());
		son.hardChoice(new QQ());
	}
}

单分派是根据一个宗量对目标方法进行选择
动态分派传入参数编译时已经确定只需要再确定时Father 还是Son

多分派则是根据多于一个宗量对目标方法进行选择。

静态分派要确定是Father 还是 SON 还要确定 传入的参数 例如

动态类型语言支持

obj.println(“hello world”) Java语言在编译期间却已将println(String)方法完整的符号引用
动态语言不会。
动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,譬如C++和Java等就是最常用的静态类型语言。obj.println(“hello world”);这个符号引用包含了该方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,Java虚拟机就可以翻译出该方法的直接引用。而ECMAScript等动态类型语言与Java有一个核心的差异就是变量obj本身并没有类型,变量obj的值才具有类型,所以编译器在编译时最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(即方法接收者不固定)。

Java 中的重载(overloading)和重写(overriding)是两种不同的多态机制,它们的区别和联系如下:

重载是指在同一个类中,定义了多个方法名相同,但是参数列表不同(参数个数或类型或顺序不同)的方法,这些方法可以有不同的返回类型和访问修饰符,但是必须有一个独一无二的参数类型列表。重载是一种静态多分派,也就是说,编译器在编译时就根据方法名和参数类型来确定调用哪个方法,不需要考虑运行时的对象类型。因此,重载有两个决定因素,即方法名和参数类型,这就是所谓的两个宗量。
重写是指在子类中,定义了一个和父类中某个方法签名完全相同(方法名和参数类型都相同)的方法,这个方法可以有不同的实现逻辑,但是必须遵守一定的规则,例如不能降低访问权限,不能抛出更宽泛的异常,不能改变返回类型(除非是协变返回类型)。重写是一种动态单分派,也就是说,编译器在编译时只根据方法名来确定调用哪个方法,而在运行时根据实际的对象类型来确定调用哪个方法。因此,重写只有一个决定因素,即方法名,这就是所谓的一个宗量。

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值