JVM发展史,虚拟机发展史模块
java技术体系包括了几个组成部分?

dsadasdsadsadasdasd在这里插入图片描述

javaME、SE、EE分别是什么?

dsad ①、ME 是支持Java程序在 移动终端 上的平台,应用:蜂窝电话、可视电话、数字机顶盒、汽车导航系统等等。注意:Android可不属于JavaME

dsad②、SE 是面向桌面级应用的Java平台,提供了完整的Java核心API

dsad③、EE 支持使用多层架构的 企业应用 ,JDK6之前被称为J2EE,捐赠给了Eclipse基金会管理。**

都说JDK7版本是第一个里程碑版本,为什么?

①、Lambda表达式项目(其实这里本来是想JDK7完成的,但是最后实际是在JDK8完成,并且在JDK8移除了HotSpot的永久代);
②、动态语言支持 等,毕竟Java是静态语言,但可为其他运行在Java虚拟机上的动态语言提供支持;

什么是动态类型语言?什么是动态语言?什么是强类型语言?

1、静态类型语言:变量定义时有类型声明的语言:变量的类型在编译的时候确定,在运行时不能修改
2、动态类型语言:它的类型检查的主体过程是在运行期而不是编译期进行的。(在运行期才确定类型,这可以为开发人员提供极大的灵活性。)
3、强类型语言:强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。
4、数据类型可以被忽略的语言
5、静态语言:在编译期间对数据类型进行检查的语言,比如Java
6、动态语言: 运行期间,允许改变程序结构(例如引进新函数、删除旧函数)或变量类型。比如Python.

Java是 强类型,静态类型,静态语言(现在也支持动态语言,具体原理后面会专门列出)。

Java语言是属于哪家公司所有?

Java语言本身不属于任何公司所有,它由JCP组织进行管理

虚拟机分为几类执行引擎?(2类)==》即时编译器优化的代码放在哪里?

1、 解释型: 一行一行执行代码,类似于javascript、python这类解释型的编程语言;

2、 及时编译(Just-In-Time) 执行引擎: 将字节码中的热点代码编译成机器码,并且将机器码缓存到方法区的代码缓存区 )

“Java语言很慢”的 原因?(由第一款虚拟机引起)

dasdas由于第一款虚拟机 Sun Classic VM 内部 只提供解释器,使用JIT编译器 需要外挂,并且一旦使用JIT就会导致解释器不能执行;由于解释器和编译器不能配合工作,这就意味着如果要使用编译执行,编译器就不得不对每一个方法、每一行代码都进行编译,而无论它们执行的频率是否具有编译的价值。基于程序响应时间的压力,这些编译器根本不敢应用编译耗时稍高的优化技术,因此这个阶段的虚拟机 虽然用了即时编译器输出本地代码,其执行效率也和传统的C/C++程序有很大差距,“Java语言很慢”的 印象就是在这阶段开始在用户心中树立起来的。

HotSpot虚拟机以及ExactVM有什么先进的技术?

dasdas热点探测(基于计算器的热点探测、基于采样的热点探测(虚拟机栈栈顶方法出现的次数))、两级即时编译器(C1, C2)、编译器与解释器混合工作模式 等。

为什么使用了准确式内容管理,就可以提高性能了?===》不使用句柄方法的原因是什么

dasdas原因:因为当GC后对象将可能会被移动位置,如果地址为123456的对象移动到654321,在没有明确信息表明内存中哪些数据是引用类型的前提下,那虚拟机肯定是不敢把内存中所有为123456的值改成654321的,所以要使用句柄来保持引用值的稳定),而”准确式内存管理“是指可以知道内存中某个位 置的数据具体是什么类型。有能力分辨出它到底是一 个指向了123456的内存地址的引用类型还是一个数值为123456的整数,这时候我们就可以判断是否能回收了。这样每次定位对象都少了一次间接查找的开销,显著提升执行性能。

HotSpot的即时编译和标准即时编译、栈上替换编译的关系?

dasdasHotSpot虚拟机的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后 通知即时编译器以方法为单位进行编译。如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准即时编译栈上替换编译(On-Stack Replacement,OSR) 行为。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更复杂的代码优化技术,输出质量更高的本地代码。

JRocket VM 的优点是什么?IBM J9的优势是什么?

dasdasJRocket VM是真正意义的世界上最快的java虚拟机。专注于服务端应用,因此JRockit内部不包含解释器实现,全部代码都靠及时编译器(JIT)编译后执行,因为他不考虑程序启动速度。

dasdasJ9的市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM;IBM J9虚拟机的职责分离与模块化做得比HotSpot更优秀。

Open JDK的由来?

dasdasApache Harmony由IBM和Intel联合开发的一款开源java,后IBM抨击Sun公司不开源java;IBM希望Apache Harmony成为java的规范,于是Sun公司开源了java并命名为OpenJDK;

Android中主要的虚拟机是什么?

dasdasDalvik虚拟机曾经是Android平台的核心组成部分之一,Dalvik虚拟机并不是一个Java虚拟机,它没有遵循《Java虚拟机规范》,不能直接执行Java的 Class文件,使用寄存器架构而不是Java虚拟机中常见的栈架构。但是它与Java却又有着千丝万缕的联系,它执行的DEX(Dalvik Executable)文件可以通过Class文件转化而来,使用Java语法编写应用程序,可以直接使用绝大部分的Java API等

TaoBao中主要的虚拟机是什么(思想很好)(ZemGc也查不到啊)?

在这里插入图片描述

Graal Vm的核心技术?以及替代位置?比C2的优点,C1和C2的区别?参数启用?部分逃逸?激进预测性优化?

dasdasHotspot虚拟机中有两个即时编译器,分别是编译耗时短但输出代码优化程度较低的客户端编译器(简称为C1)以及编译耗时较长但输出代码优化质量也较高的服务端编译器(简称为C2),通常它们会在分层编译机制下与解释器互相配合来共同构成Hotspot虚拟机的执行子系统
dasdas自JDK 10起,HotSpot中又加入了一个全新的即时编译器:Graal编译器。Graal编译器是以C2编译器替代者的身份登场的。Graal编译器本身就是由Java语言写成,实现时又刻意与C2采用了同一种名为“Sea-of-Nodes”的高级中间表示(High IR)形式。 使其能够 更容易借鉴C2的优点。Graal能够做比C2更加复杂的优化,如“部分逃逸分析”(Partial Escape Analysis),也拥有比C2更容易使用激进预测性优化(Aggressive Speculative Optimization)的策略,支持自定义的预测性假设等

dasdas-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 参数来启用Graal编译器

dasdasGraal VM是在HotSpot VM基础上增强而成的 跨语言全栈虚拟机。它支持不同语言中混合使用对方的接口和对象,支持这些语言使用已经编写号的本地库文件;
在这里插入图片描述
dasdas[注] :通过解释器构建中间表示形式的过程,称为 程序特化
在这里插入图片描述

Java的三种编译方式?(前端,后端,提前编译)(泛型、内部类等是通过前端编译实现的)

在这里插入图片描述

热点探测技术有哪几种?

在这里插入图片描述

JVM之内存区域模块
Java与C++之间的内存区域有什么区别?(内存动态分配和垃圾回收技术等等)
我们为什么要在虚拟机层面学习内存泄漏和溢出的相关知识呢,虚拟机管理内存不是很美好嘛?

dasdas虽然我们把控制内存的权力交给了Java虚拟机,它管理的也非常好,但是如果出现了内存泄漏和内存溢出问题,如果你不了解虚拟机如何使用内存的话,那么你排查起错误、修正错误会让你非常头疼的。

java的运行时数据区域?

dasdas 6块区域:程序计数器,虚拟机栈,本地方法栈、堆、方法区、直接内存
在这里插入图片描述
dsasadasdas在这里插入图片描述

程序计数器的用法?搭配什么执行引擎使用的?为什么是线程私有的?有没有OOM?

在这里插入图片描述
das使用PC寄存器存储字节码指令地址有什么用呢?|| 为什么使用PC寄存器记录当前线程的执行地址呢?
dasdas因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
dasPC寄存器为什么会设定为线程私有?
dasdas我们都知道所谓的多线程在一个特定的时间段内指会执行其中某一个线程的方法,CPU会不停滴做任务切换,这样必然会导致经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
dasdas由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
dasCPU时间片?
dasdasCPU时间片即CPU分配各各个程序的时间,每个线程被分配一个时间段。称作它的时间片。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

Java虚拟机栈中包含什么?栈帧又包含什么?局部变量表包含哪三部分?虚拟机栈可不可以扩展?槽的介绍?栈有没有最小值?

在这里插入图片描述


本地方法栈中主要用于执行什么方法?会不会出现两种异常情况?(它和虚拟机栈一样的)

在这里插入图片描述

堆可以如何分配?所有对象都都是在堆上建立吗?堆的相关参数(-Xms Xmx)?

在这里插入图片描述

方法区主要放的什么?gc主要回收什么?方法区中三个常量池的不同点?异常都能发生么?

在这里插入图片描述

直接内存中常用的类?NIO在直接内存的使用?Unsafe方法与varHandle的关系?

在这里插入图片描述

普通对象的创建大致分为几步?

dasdassadasdasdasdasdasdadsa在这里插入图片描述
dasdas1、当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在运行时常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程;

dasdas2、在类加载检查通过后,接下来虚拟机将为新生对象分配内存对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来

dasdas3、Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头。

dasdas4、这时候从虚拟机角度:对象已经产生;但是从Java程序角度: 刚刚开始,因为需要构造函数,即 Class文件中的 < init >() 方法 还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。

普通对象创建的第三步,哈希码是什么时候计算的?(当对象真正调用时,才计算出)
普通对象创建的第二步,内存分配的方式有哪几种?在划分空间时如何保证线程安全?TLAB的启动参数?

在这里插入图片描述

普通对象创建的第四步,是根据什么设置对象头信息(偏向锁)?静态变量需要使用TLAB吗(不需要)(复盘完类加载机制再回看)?

在这里插入图片描述

对象的实例字段在java代码中可以不赋值直接使用吗?(可以!)(后面类加载机制有详解)

在这里插入图片描述

public class Main{
    int b;
    public static void main(String[] args) {
        number i = new number();
        number j = new number();
        System.out.println(i.i);//直接打印出默认值。
        System.out.println(j.i);//直接打印出默认值。
        i.i = 1;
        j.i = 3;
        System.out.println(i.i);
        System.out.println(j.i);
    }
}
class number {
     int i;//声明全局变量,因为在类中,所以是全局变量,不用赋初值,默认的值是0.注意全局变量如果没有赋初值,都有默认值,例如String类型的默认值是null.
    public int get(){//定义一个方法。
        System.out.println(i);
        int num1;//声明局部变量,在方法内部所以是局部变量,没有使用,所以可以不用赋初值,如果使用的话必须赋初值。
        int num3=3;//声明局部变量,用来作为返回值,使用了,所以要初始化,否则不能通过编译。
        //System.out.println(num2);   //错误因为num2没有初始化就使用。
        return num3;
    }
} 输出:0	0	1	3
对象在堆中存储布局分为几部分(3)?

在这里插入图片描述

对象的访问定位?句柄方法和直接指针的优缺点?(句柄再考虑一下)

dasdasJava程序会通过栈上(栈帧–>局部变量表)的reference数据来操作堆上的具体对象,而主流的访问方式大致分为两种:①、使用句柄②、直接指针。
dsadas在这里插入图片描述
在这里插入图片描述

不同jdk版本的string::intern()问题

在这里插入图片描述
dasdas对于new String() + new String(),底层优化都是StringBuilder.append然后toString(),toString是创建一个新得字符串,但是不会放入到常量池中,调用相应的intern之后,方法池中保存的是对StringBuilder的引用。

String str = "aaa"与String str = new String(“aaa”)一样吗?后者创建了了几个对象?new String + new String 底层优化是什么?(TODO)
JVM之类加载机制模块
类加载机制的过程是什么?这样的加载机器有什么优缺点呢?Java动态扩展的语言特性通过什么实现?

dasdas过程 :JVM把描述类的数据从Class文件中 加载到内存中,并对其进行 校验, 解析, 初始化,最终就会形成能被JVM直接使用的Java类型,这个过程就是虚拟机的类加载机制。

dasdas优缺点:与其他语言在编译期间就需要连接不同,在Java中类型的加载、连接和初始化都是在程序运行期间完成的,虽然这样为Java应用 提高了极大的 扩展性和灵活性,但是 提前编译(AOT)技术就有了困难(因为需要是在运行前编译),以及在 类加载时会稍微增加一些性能开销。(AOT后面涉及)

dasdasJava天生可以动态扩展的语言特性就是 依赖 运行期动态加载和动态连接 这个特点实现的。

从类加载机制说明类的生命周期?一共有几步?顺序固定吗?(七个阶段)

dasdas加载,验证,准备 ,解析,初始化,使用,卸载
dsdsdsdasd在这里插入图片描述

dasdas加载、验证、准备、初始化和卸载这五个阶段的"开始顺序"是确定的是确定的,而执行过程其实是 相互交叉混合 进行的。

dasdas而解析阶段则不一定(因为有时可以在初始化阶段之后再进行);

第一个阶段"加载"并有什么强制约定吗?(没有)
”初始化阶段“有什么约定吗?(比如”主动引用“)

dasdas 《Java虚拟机规范》则是严格规定 有且只有六种情况可以对类进行初始化 ,俗称"主动引用"。(初始化之前,加载、验证、准备阶段已经完成)

dasdas 1、遇到 new 、 getstatic、 putstatic或 invokestatic这四条字节码指令 时,该类要先初始化 (对用的就是 new 实例化对象的时候,读取或设置一个静态字段,调用一个类的静态方法)

dasdas 2、通过用java.lang.reflect包对类进行 反射 调用的时候;

dasdas3、初始化该类时,如果有父类且没有初始化,则 先触发父类的初始化

dasdas4、VM启动时,会首先对主类(包含main()方法的类)进行初始化;

dasdas5、JDK7以后,通过 动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类的 方法句柄,且对应的类没有初始化,则需要先触发其初始化。
dasdas6、被default关键字修饰的接口,如果该接口的实现类发生了初始化,那在这之前要先初始化该接口。

”常见的几种”被动引用“有什么?(就是不需要初始化的情况)(主要三种)(static final修饰的属性在编译阶段就确定了吗?)

dasdas1、通过子类引用父类的静态字段,不会导致子类初始化 ,只有直接定义这个字段的类才会被初始化,至于是否要触发子类的加载和验证阶段,在《Java虚拟机规范》中并未明确规定,所以这点取决于虚拟机的具体实现。
dasdsdssd在这里插入图片描述
dasdas2、通过数组定义来引用类,不会触发此类的初始化
dasdsdsddsdssdsdsdss在这里插入图片描述
在这里插入图片描述

dasdas3、常量(final修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
dsdsddasd在这里插入图片描述
在这里插入图片描述

接口初始化和类初始化的过程有哪些区别?

dasdas接口的加载过程与类加载过程稍有不同,针对接口需要做一些特殊说明:接口也有初始化过程,这点与类是一致的,但接口中不能使用“static{}”语句块,不过编译器仍然会为接口生成“< clinit >()”类构造器,用于初始化接口中所定义的成员变量。

dasdas接口与类的六种“主动引用”就有一个不同,即当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载的 “加载” 阶段要完成几项功能?其中哪个阶段人员可开发性最强?

dasdas通过一个类的全限定名获取定义此类的二进制流;

dasdas⒉将这个字节流所代表的 静态存储结构 转化为 方法区的运行时数据结构;

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

dasdas最灵活的阶段:第一个阶段,因为并没有规定这个二进制流必须从哪里(比如Class文件)获取,以及如何获取,可以从ZIP压缩包中获取,从网络中获取,可以运行时计算生成(动态代理技术),从加密文件获取等。

对于数组类的“加载阶段”有什么要注意的地方吗

在这里插入图片描述

加载阶段和连接阶段可以交叉进行么?

dasdas 加载阶段与连接阶段的部分动作是交叉进行了,加载阶段尚未完成,连接阶段可能已经开始。

类加载的 “验证“阶段主要做的什么?

dasdas主要是为了确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,JVM如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟机保护自身的一项必要措施。

dasdas大致分为四个阶段:①、文件格式的验证 ②、元数据的验证 ③、字节码验证 ④、符号引用验证。
在这里插入图片描述

”验证“阶段是必须要存在的吗?(不是必须,但是很重要)

dasdas验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了。

“符号引用验证”阶段是发生在哪个阶段进行的? (解析阶段)
类加载的 “准备“阶段主要做的什么?(实例变量什么时候初始化?)

dasdas准备阶段是正式为 静态变量 分配内存并设置初始值 (数据类型的零值) 的阶段。同时也会初始化方法区的虚方法表,这样能提高重写时查找元数据的效率。

dasdas实例变量 将会在对象实例化的时候随着对象一起分配在Java堆中。

对于static 和 final 同时修饰的属性,什么时候开始初始化?

dasdas如果类字段的字段属性表中存在 ConstantValue属性(final和static同时修饰),那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。(在编译时期就被放入到了常量池中)

类加载的 “解析“阶段主要做的什么?
  • 解析阶段是JVM将常量池内的符号引用替换为直接引用的过程;(符号引用–>直接引用)

    符号引用和直接引用分别是什么?

    在这里插入图片描述

    解析阶段发生的时间?访问权限的检查发生在哪个阶段?

    dasdasVM可以根据需要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。
    dasdas对方法或者字段的访问,也会在解析阶段中对它们的可访问性(public、protected、private、< package >)进行检查。

    对于符号引用的解析可以不可以说成具有一致性?(可以吧)

    在这里插入图片描述

    对于类或接口、字段、类方法、接口方法这四种符号引用进行解析 的流程?如何保证了字段的唯一性?

在这里插入图片描述

说说你对初始化阶段的理解?

sadasd类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,JVM才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
sadasd在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
sadasd初始化阶段就是执行 类构造器< clinit >() 方法的过程。该方法并不是程序员在Java代码中直接编写的,而是Javac编译器的自动生成物。

说说对 < clinit > 的使用?是必须要的?类和接口中有什么不同?

sadasd1、< clinit >()方法是由编译器自动收集类中的所有 类变量 的赋值动作和 静态语句块(static{}块) 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,
sadasd2、静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
sadasd3、< clinit >()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成< clinit >()方法。
sadasd4、接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成< clinit >()方法。但接口与类不同的是,执行接口的< clinit >()方法不需要先执行父接口的< clinit> ()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的< clinit >()方法。
sadasd5、虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit >()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit >()方法完毕。如果在一个类的< clinit >()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

sdsddsdsdsdsdsada在这里插入图片描述

说说< clinit > 和 < init >的不同?

sadasd< clinit >()方法与实例构造器< init >()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的< clinit >()方法执行之前,父类的< clinit >()方法已经执行完毕。因此,在虚拟机中第一个被执行的< clinit >()方法的类肯定是java.lang.Object。由于父类的< clinit >()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

静态代码块执行的最强面试题?
public class JavaTest {
    public static void main(String[] args) {
        f1();
    }
    static JavaTest javaTest = new JavaTest();
    static {
        System.out.println("1");
    }
    {
        System.out.println("2");
    }
    JavaTest() {
        System.out.println("3");
        System.out.println("a=" + a + ", b=" + b);
    }
    public static void f1() {
        System.out.println("4");
    }
    int a = 100;
    static int b = 200;
}

sadasd分析:
在这里插入图片描述

类加载器有什么作用?class文件相同加载的类就一样吗?

asda对于任意的一个类,都必须由加载它的类加载器➕这个类本身一起共同确立其在JVM中的唯一性。每一个类加载器,都拥有一个独立的类名称空间。

asda换种说话就是:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个JVM加载,只要加载它们的类加载器不同,这两个类也不相等。

ass【注】:这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。

JVM中有几类类加载器呢?(关于类加载器的讨论,主要基于JDK8即之前的版本进行分析)

d 其实从JVM的角度看,大致可以分为两类:
d s ①、启动类加载器(Bootstrap ClassLoader): JVM自身的一部分,由C++实现的。(只针对HotSpot虚拟机,JDK9以后也采用了类似的虚拟机与Java类互相配合来实现Bootstrap ClassLoader的方式,所以HotSpot也有一个无法获取实例的代表Bootstrap ClassLoader的Java类存在
ads②、其他所有的类加载器: 全部由Java实现,独立存在于VM外部,并全都继承自抽象类java.lang.ClassLoader

dss【注】:从Java开发人员的角度来看,类加载器分的更细致。自JDK1.2以来,Java一直保持着三层类加载器双亲委派的类加载器结构

什么是双亲委派模型?双亲委派模型的工作流程是什么?

ddsd 每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。

dssss如果一个类加载器收到了类加载的请求,它首先会把这个请求委派给“父类”加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
dsadasdsadasdasdsadasdsads在这里插入图片描述

双亲委派模型中重要的三个类加载器是什么?

dssss1、启动类加载器(引导类加载器,BootStrap ClassLoader):最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。Object类在标准库lib目录下,由Bootstrap Classloader加载。
dssss2、ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
dssss3、AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。 父类加载器为扩展类加载器。
dasdasdadsadasd在这里插入图片描述

自定义类加载器的实现?

在这里插入图片描述

为什么还需要自定义类加载器?

dssssJDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的。当然我们也可以加入自定义的类加载器进扩展。比如:
dssssss ①、增加存储于磁盘之外的Class文件来源;
as
dssssss ②、实现类的隔离、重载等功能;

dssssss③、修改类加载的方式

自定义类加载器的应用场景?(Tomcat等)

在这里插入图片描述

"双亲委派模型"的好处是什么?

dssss1、保证java类型体系,避免类重复加载 :Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系;例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之如果没有该模型,那么不同的类加载器可能会加载同一个名为java.lang.Object的类,那么 在程序的classPath上就会有不同的Object类,那么我就不能保障Java类型体系了。
dssss2、沙箱安全机制:如果有人想替换系统级别的类:String.java是不会被加载的,这样便可以防止核心API库被随意篡改。

双亲委派模型的核心代码:
protected synchronized Class<?> loadClass(String name, boolean resolve) throwsClassNotFoundException
{
	// 首先,检查请求的类是否已经被加载过了
	Class c = findLoadedClass(name);
	if (c == null) {  //如果未加载过就交给父类加载器
		try {
			if (parent != null) {
			c = parent.loadClass(name, false);
			} else {
				c = findBootstrapClassOrNull(name);
			}
		} catch (ClassNotFoundException e) {
		// 如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求
		}
		if (c == null) {
			// 在父类加载器无法加载时,再调用本身的findClass方法来进行类加载
			c = findClass(name);
		}
	}
	if (resolve) {
		resolveClass(c);
	}
	return c;
}

dssss逻辑解析:
dssasdasss ①、检查请求加载的类是否已经被加载过;
dssasdasss ②、如果没有则调用父加载器的loadClass()方法;
sasdasd ss ③、如果父加载器为空,则默认使用启动类加载器为父加载器;
dasdasd ss ④、如果父加载器失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法尝试进行加载。

"双亲委派模型"的要求是什么?

dssss①、顶层必须是启动类加载器;

dssss②、其余的类加载器都有自己的父类加载器;注意它们之间不是继承关系,而是通常使用组合(Composition)关系来复用父加载器的代码。

谈一谈“双亲委派模型的四次破坏” ?

在这里插入图片描述
dssss第四次破坏:
在这里插入图片描述

JDK9版本的一些类加载有什么不同?

在这里插入图片描述

JVM之垃圾回收机制
哪些内存(垃圾)需要回收呢?(堆、方法区)

在这里插入图片描述

堆中的哪些数据(内存)是需要回收的(先看堆)?

dssss堆里面几乎存放着所有的对象实例,而垃圾回收机器在对堆进行回收时,首先要做的就是确定这些对象值中哪些是“活着”的,哪些是"死去"的。我们要回收的,就是“死去”的对象。

如何判断哪些对象是"死去"的(两种算法)?

在这里插入图片描述

固定可作为GC Roots的对象有哪几种?(七种)

dsadas在这里插入图片描述

什么叫引用呢?(jdk1.2之后的引用:虚、弱、软、强)弱引用的例子?虚引用的作用?系统通知?谁容易造成内存溢出?各自的应用在哪里?

dssss强引用 :无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。强引用所指向的对象在任何情况下都不会被回收,JVM宁愿抛出OOM,也不会回收强引用所指向的对象。所以,强引用是造成Java内存泄漏的主要原因之一。

dssss软引用 :是用来描述一 些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。非强引用是不会导致内存溢出的,因为被回收了软引用通常用来实现内存敏感的缓存

dssss弱引用 :只要GC就回收,发现即回收,弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。弱引用对象更容易、更快被GC回收。(这里可以提到弱引用的应用,weakedHashMap,然后扯到ThreadLocal,然后再到它在框架中的应用)

dssss虚引用:与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虛引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

对于“不可达”对象,就可以判定其能进行GC了?Finalize方法能调用几次(1次)?

答:当然不是的,法律都有缓刑,那我们GC机制当然也有啦,其实要真正判定一个对象“死亡”,至少要经过两次标定过程的,大致过程如下:
saass①、首先,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
saass②、标记后根据 finalize() 方法来判断是否需要进行第二次标记;
saass③、如果没有使用过finalize()就进入 F-Q队列 中等待虚拟机建立Finalizer线程去执行它们的finalize()方法(如果对象没有重写这个方法或者已经被虚拟机执行了相应的finalize()方法就不用缓刑(不用进入F-Q队列)了,直接死刑!),如果在执行finalize()方法中成功与引用链上的任何一个对象建立关联,那么恭喜你,不用执行死刑(GC)了!!! (可在 jvisual中查看)

对象可以在被GC时自我拯救,不过这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次。

什么叫Finalizer线程?

saassFinalizer线程是虚拟机自动建立的、低调度优先级的线程;并且该线程虽然会执行F-Q队列中的各个对象的finalize()方法,但是不代表会等待它执行完,毕竟如果这个方法执行特别缓慢,或者死循环等,会使整个内存回收系统崩溃

saassfinalize() 能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时,所以这个方法大家可以不用使用。

方法区如何GC呢 ?对什么GC呢?(常量、类)

方法区的垃圾收集主要回收两部分内容:①、废弃的常量 ②、不再使用的类
【注】:
sss⒈对于常量是否”废弃“是很简单的, 我们假设一个字符串“java”曾经进入常量池中,但是已经没有任何字符串对象引用常量池中的“java”常量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。(JDK7以后,字符串常量池就移到了堆里)
sss⒉对于类是否”不再被使用“的判断要相对复杂一些,需要同时满足三个条件:
ss22s⑴Java堆中不存在该类及其子类的实例;
ss22s⑵加载该类的类加载器已经被回收(该条件一般很难达成);
ss22s⑶该类对应的java.lang.Class对象没有在任何地方被引用(无法通过反射反应该类);
s22s注意:当满足上述三个条件时,仅仅是被同意回收,而是否需要对其回收,虚拟机提供了-Xnoclassgc参数进行控制;
s22s常识:在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP、以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

以什么方式进行回收呢

从如何判定对象消亡的角度出发,垃圾收集算法可以分成两大类:
sasadsadasdsadaq12313 12①、引用计数式垃圾收集 adsdad2 ②、追踪式垃圾收集

当前商业虚拟机的垃圾收集器,大多数都遵循了 “分代收集” 的理论进行设计(分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为 新生代和老年代两个区域),它建立在三个分代假说之上:
sasa①、弱分代假说:绝大多数对象都是朝生夕灭的;
sasa②、强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡;
sasa③、跨代引用假说:跨代引用相对于同代引用来说仅占极少数,这也是分代收集的难点

sasa常用的垃圾收集器的一致设计原则:根据三个分代假说理论,收集器将Java堆划分出不同的区域,然后将回收对象依据其年龄(对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。垃圾收集器每次只回收其中一个或某些区域,并且我们可以根据不同区域设计与该区域存储对象存亡特征相匹配的GC算法,这样就好处就是提高了GC效率。

可以谈谈跨代引用吗?跨代引用为什么占少数?怎样提高跨代引用带来的回收影响(“记忆集”)?TLAB相同思想?

sasa 跨代引用占少数的原因:如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

sasa针对跨代引用的措施 :我们只需要在新生代 建立一个全新的数据结构-—“记忆集”,这个结构把老年代划分成若干小块,标记出老年代的哪一块内存存在跨代引用。当再发生年轻代GC时,只有包含了跨代引用的小块内存里的对象才被加入到GC Roots进行扫描,虽然记忆集也需要有额外的开销:需要维护记录数据的正确性,但是这样相对于扫描整个老年代也是提高了效率。 (思想和TLAB一样)

记忆集具体怎么实现的?

我们所需的记忆集不需要具体判断是哪一个对象跨代引用,而是判断哪一块非收集区域是否存在有指向收集区域的指针就可以了。所以我们可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本。
【记录精度】:
sasa ⒈字长精度:每个记录精确到一个机器字长(一般是8的整数倍,32位或者64位,机器字长是指计算机进行一次整数运算所能处理的二进制数据的位数),这个精度决定了机器访问物理内存地址的指针长度,该字包含跨代指针。
sas a⒉对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
sa sa⒊卡精度(“卡表”):每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
【注】:
sasaⅠ、 第三种“卡精度”所指的是用一种称为“卡表”的方式去实现记忆集,这也是目前最常用的一种记忆集实现形式。前面定义中提到记忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。
sasaⅡ、 卡表最简单的形式可以只是一个字节数组,该数组的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”。一般来说,卡页大小都是以2的N次幂的字节数,HotSpot中使用的卡页是2的9次幂,即512字节(之所以使用byte数组而不是bit数组主要是速度上的考量,现代计算机硬件都是最小按字节寻址的,没有直接存储一个bit的指令,所以要用bit的话就不得不多消耗几条shift+mask指令。
sasaⅢ、 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。


卡表是怎么维护的?维护手段是什么?卡表带来的问题有什么?怎么解决伪共享问题?所用的参数?

⑦、(卡表的维护问题)上一个问题讲到了"卡表",也就是记忆集的具体实现,来缩短了跨代引用带来的GC Roots的扫描范围问题,但是这个卡表如何维护(它们何时变脏,谁把它们变脏等)呢?

  • 其实很容易得到答案,当然是有非收集区域引用了收集区域的对象,我们就把非收集区域对应的卡表元素变脏。变脏时间点原则上,应该发生在引用类型字段赋值的那一刻。
    【注】:在引用类型字段赋值的那一刻,我们也要对其区分:
    sasssa⒈ 如果是解释执行的字节码: 虚拟机负责每条字节码的执行,这时候就有充分的介入空间,去(维护)更新卡表;
    sasssa⒉如果是JIT(即时编译)后的代码: 这时候已经是机器码,跳过了字节码,这时候我们就不能以字节码的形式去维护卡表;
    sasssa总结:经过⒈和⒉的分析,我们可以得知,为了统一,我们应该找到一个在机器码层面的手段,把维护卡表的动作放入到每一个赋值操作值中。
  • 这个机器层面来维护卡表的手段是什么呢?写屏障。
    【注】:
    sasssa⒈ 写屏障: 可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。
    sasssa ⒉应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令, 一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新, 就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。
  • 写屏障带来的问题: 卡表在高并发场景下面临着 “伪共享” 问题。伪共享是处理并发底层细节时一种经常需要考虑的问题CPU的缓存系统中是以缓存行为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。
    【举例】:
    sasssa 假设一个CPU的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。
    【解决方法】:
    sasssa ⒈、 为了避免伪共享问题,一种简单的解决方案是:而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏;

    sasssa ⒉、JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断;
三种GC回收算法:

sadaq12asdasdas12 ①、标记-清除算法 sadsad ②、标记-复制算法 sadsad ③、标记-整理算法

标记-清除算法: (适用于老年代)
  • 顾名思义,算法分成两个阶段: ①、标记 ②、 清除
    • 首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
  • 两大缺点:
    sasa⒈ 执行效率不稳定, 如果此GC区域存在大量对象需要被GC,那么需要执行大量的标记和清除动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低(说明不适应新生代区域的GC)。
    sasa内存空间的碎片化 问题, 标记-清除之后会产生大量不连续的内存碎片,当我们对较大对象(比如数组)无法分配足够大的连续连续内存时,就不得不提前触发一次GC。
标记-复制算法:(适用新生代、主流,优化)(“逃生门思想”)(新生代)
  • 由来: 这个算法是在标记-清除算法的基础上解决其执行效率低以及产生空间碎片而提出的,可以称其为”半区复制“。顾名思义,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

  • 优点:实现简单,运行高效,只要移动堆顶指针,按顺序分配即可,不用考虑有空间碎片的复杂情况。

  • 缺点:如果大部分对象是存活的,那么复制就会产生很大的开销,也就是说适用于新生代GC,不过内存缩小为了原来的一半,造成了空间浪费。

  • 优化:
    sasa①、优化场景:现在的商用Java虚拟机大多都优先采用了这种标记-复制算法去回收新生代,并且对这种算法进行了优化。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种优化的策略来设计新生代的内存布局。
    sasa②、优化思想:因为针对不同的区域需要使用不同的算法GC,而对于新生代的大部分对象都是一次性的,所以没必要对新生代一半一半分,我们只要留下一点空间来当作存储存活对象的区域即可,而如果留下的区域不够用的时候我们就把其放入到老年代中,毕竟新生代不够用了。
    sasa③、优化的具体实现:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生GC时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间,当Survivor空间不足以容纳一次新生代GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(我们把这种分配担保称为"逃生门"设计)。

标记-整理算法:(适用老年代)(STW)
  • 由来:标记-复制算法在对象存活率较高时就要进行较多的复制操作,所以效率将会降低。 更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法,针对老年代对象的存亡特征,提出了“标记-整理”算法。
  • 对比:“标记-整理”算法可以看成是对”标记-清除“算法的优化,标记完后排列,然后集中清除需要回收的,也就是分三步:
    sasaasasdasdasddasdsadsadas①、标记 ②、排列 ③、清除
  • 分析:
    sasa ⒈ 在老年区这种每次GC都有大量对象存活的区域,如果使用"标记-整理"算法意味着要移动大量存活的对象并更新引用这些对象的所有地方,这其实是一种极为负重的操作,并且这种操作需要全程暂停用户应用程序才能进行,也就是"Stop The World"(STW);
    sasa⒉但是如果我们不考虑移动这些对象,还是按”标记-清除“算法那样,标记完直接清除,那样产生的空间碎片化问题会需要更复杂的内存分配器来解决。
碎片化问题的解决方案

sasa通过“分区空闲分配链表”来解决内存分配问题(计算机硬盘存储大文件就不要求物理连续的磁盘空间,能够在碎片化的硬盘上存储和访问就是通过硬盘分区表实现的);

对于老年代算法选择的衡量(停顿时间STW 和 高吞吐量)?

sasa⒊ 通过以上分析,是否移动对象都存在弊端,移动则内存回收时(需要 更新引用存活对象 的所有地方)会更复杂,不移动则内存分配时 (碎片化问题) 会更复杂。从不同的角度看:
sasaaa ㈠、从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿;
sasasa ㈡ 、从整个程序的吞吐量来看,移动对象会更划算;
sasasa 总结:"标记-整理"比"标记-清除"算法的吞吐量要高一些,所以因地制宜,看你的收集器侧重用哪方面,如果侧重延迟则选择"标记-清除"算法;如果侧重吞吐量方面,则选择"标记-整理"算法;

  • 优化:"标记-清除"和"标记-整理"算法折中的使用就是先按"标记-清除"算法执行,当造成的空间碎片最大化,已经无法忍受时,我们再进行"标记-整理"算法GC一次

GC细节问题:
(GC Roots枚举问题)在可达性分析中,在GC Roots中查找相应的引用链需要STW吗?

调用栈里的引用类型数据是GC的根集合(root set)的重要组成部分;找出栈上的引用是GC的根枚举(root enumeration)中不可或缺的一环。

  • 迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,也就是"STW"。 虽然现在的可达性分析算法中耗时最长的查找引用链的过程也已经可以做到与用户线程一起并发,但 根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行,这是导GC过程必须STW的一个重要原因。

    【一致性】: 这里“一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况,而这也是满足结果准确性的必要条件。


(GC Roots枚举问题)在GC Roots中查找相应的引用链时,需要检查所有的引用位置吗?(不需要)

sasa由于目前主流Java虚拟机使用的都是准确式垃圾收集,所以当STW时,也就是用户线程停顿下来以后,其实并不需要检查所有的引用位置 ,虚拟机是有办法直接得到哪些地方存放着对象引用的。
sasa比如,在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。对象的类型信息里有记录自己的OopMap,一旦类加载动作完成的时候HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来在JIT过程中,也会在特定的位置(安全点)记录下栈和寄存器里哪些位置是引用。(记录了栈上局部变量表中reference到堆上对象的引用关系) 这样GC器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。 (空间换时间的思想)


什么叫准确式呢?对应的保守式是什么?半保守式你了解吗?

sasa保守式:JVM选择不记录任何类型的数据,那么它就无法区分内存里某个位置上的数据到底应该解读为引用类型还是整型还是别的什么。这种条件下,实现出来的GC就会是“保守式GC(conservative GC)”。微软的JVM和javascript早期就是这种。
sasa缺点 : 部分对象本来应该已经死了,但有疑似指针指向它们,使它们逃过GC的收集,使内存占用大。由于不知道疑似指针是否真的是指针,所以它们的值都不能改写,因此引入”句柄“方式查找实际对象,早期的Classic jvm就是这样,而现在都是直接指针访问内容;(只能使用标记-清除算法)

sasa半保守式:JVM可以选择在栈上不记录类型信息(同保守式),而在对象上记录类型信息,“也成为根上保守”。为了支持半保守式GC,运行时需要在对象上带有足够的元数据。如果是JVM的话,这些数据可能在类加载器或者对象模型的模块里计算得到,但不需要JIT编译器的特别支持。

sasa代价及缺点 : 代价是优化过的DEX文件的体积膨胀了约9%。由于半保守式GC在堆内部的数据是准确的,所以它可以在直接使用指针来实现引用的条件下支持部分对象的移动,方法是只将保守扫描能直接扫到的对象设置为不可移动(pinned),而从它们出发再扫描到的对象就可以移动了。(只能使用标记-清除算法)半保守式GC对JNI方法调用的支持会比较容易:管它是不是JNI方法调用,是栈都扫过去…完事了。不需要对引用做任何额外的处理。当然代价跟完全保守式一样,会有“疑似指针”的问题。

sasa“准确式GC”:是什么东西“准确”呢?关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才可以合理地解读数据的含义;GC所关心的含义就是“这块数据是不是指针”。

sasa实现方式:
sadsdssa 1、让数据自身带上标记(tag)。栈上对每个slot都配对一个字长的tag来说明它的类型,通过这种 方式来减少stack map的开销。
ssdsdasa2、让编译器为每个方法生成特别的扫描代码。
ssdsdasa3、从外部记录类型信息,存成映射表(Oopmap)。现在三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样做的。 要实现这种功能,需要虚拟机里的解释器和JIT编译器都有相应的支持,由它们来生成足够的元数据提供给GC。使用这样的映射表一般有两种方式:
ssdsdasa1、每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
ssdsdasa2、为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。

HotSpot是用“解释式”的方式来使用OopMap的,每次都循环变量里面的项来扫描对应的偏移量。


说一说你对OopMap的理解? Oop和安全点的关系?为什么要设置安全点?要设置多少安全点?

sasa ⒈ 在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。平时这些OopMap都是压缩了存在内存里的;在GC的时候才按需解压出来使用。(可以把oopMap简单理解成是调试信息。 在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。oopMap就是一个附加的信息 ,告诉你栈上哪个位置本来是个什么东西。 这个信息是在JIT编译时跟机器码一起产生的 。因为只有编译器知道源代码跟产生的代码的对应关系。 并且每个方法可能会有好几个oopMap,就是根据safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。 循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。)
sasa2 实际上,每个被JIT编译过后的方法也会在一些特定的位置记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了。而这些特定的位置称为安全点。有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。

【安全点的选定规则】: 安全点的选定既不能太少也不能太多,太少就会让GC器等待时间过长,太多就增大了运行时的内存负荷以及维护成本。因此我们规定,安全位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,只有具有这些功能的指令才会产生安全点。


(GC roots)中本地方法栈也有引用对象,这是根据OopMap进行维护的吗?GC对于JNI何维持准确性呢?JNI调用慢的原因?

sasa对Java线程中的JNI方法,它们既不是由JVM里的解释器执行的,也不是由JVM的JIT编译器生成的,所以会缺少OopMap信息。
sasaHotSpot的解决方法是:所有经过JNI调用边界(调用JNI方法传入的参数、从JNI方法传回的返回值)的引用都必须用“句柄”(handle)包装起来。JNI需要调用Java API的时候也必须自己用句柄包装指针。在这种实现中,JNI方法里写的"jobject"实际上不是直接指向对象的指针,而是先指向一个句柄,通过句柄才能间接访问到对象。这样在扫描到JNI方法的时候就不需要扫描它的栈帧了——只要扫描句柄表就可以得到所有从JNI方法能访问到的GC堆里的对象

sasa这也就意味着调用JNI方法会有句柄的包装/拆包装的开销,是导致JNI方法的调用比较慢的原因之一。


(安全点对应的两种方案)如何在GC发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来?
  • 两种方案选择:⒈主动式中断(主流方式) ⒉抢先式中断
  • 抢先式中断的思想: 不需要线程的执行代码主动去配合,在GC时,系统首先会把所有用户线程中断,如果发现有用户线程没有停在安全点上,就恢复这条线程执行,让它一会再中断直到跑到安全点上(现在几乎没有虚拟机用抢先式中断)。
  • 主动式中断的思想: 当GC需要中断线程的时候,不直接对线程操作,而是设置一个标志位,各个线程执行过程时不停的主动去轮询这个标志位,一旦发现这个标志位为true,就把自己挂在附近的安全点上。
    【注】:标志位和安全点是重合的。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。
    【思考】: 由于轮询操作在代码中会频繁出现,所以我们要让这个操作尽可能高效,HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。

(安全点的局限性)安全点解决了如何停顿用户线程,但是那些处于Sleep状态或Blocked状态的线程无法响应虚拟机的中断请求,所以不可能自己走到安全点去中断挂起,这种情况应该怎么做呢?
  • 我们是通过 安全区域 来解决这种情况的;
  • 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的;
    【安全区域和安全点的关系】:安全区域看作被扩展拉伸了的安全点。
    【安全区域的用法】: 首先进入安全区域要标识,没离开安全区域时要检查虚拟机是否完成了GC Roots枚举,如果完成了就可以离开安全区域了(就是垃圾收集过程中需要暂停其他用户线程的阶段,只要经过了这个阶段就可以离开安全区域了,不一定非要完成GC Roots枚举),否则就一直等待,直到收到可以离开安全区域的信号为止(当线程处于睡眠或阻塞状态之前会进入安全区域?希望大神看到能解答一下)。
    在这里插入图片描述

对于GC的细节你简单说一说?

sasa在可达性分析中,在GC Roots中查找相应的引用链需要STW,但是没必要检查所有的引用位置,只需要通过在安全点的位置使用OopMap数据结构来看具体引用什么对象即可,不过在使用安全点的时候,要注意两点,其一就是让阻塞或睡眠状态的线程进入安全区域,其二就是我们通过主动式中断的思想来让线程挂在安全点上,其中用到的轮询标志位操作只需要一条汇编指令即可,并且轮询标志位和安全点是重合的。

找到相应的 GC Root后再怎么往下遍历对象图呢?如何对不"存活"的对象进行标记呢?

sass这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储的对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长,不过如果我们能消除"标记"这部分停顿的时间,那么收益是系统性的。(三色标记法)


讲一讲三色标记法?并发工作带来的问题?CMS和G1中分别怎样解决的?

sasssa ⒈白色:表示对象尚未被垃圾收集器访问过。 若在分析结束的阶段,仍然是白色的对象,即代表不可达。
sasssa ⒉黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
sasssa ⒊灰色: 表示对象已经被GC器访问过,但这个对象上至少存在一个引用还没有被扫描过(可以理解为半成品)。

  • 关于可达性分析的扫描过程,相当于对象图上以灰色为波峰的波纹从黑向白推进的过程,如果用户线程此时是冻结的,只有GC器线程在工作,那不会有任何问题,但如果用户线程与GC器是并发工作呢? 这种情况可能会出现两种错误结果:
    sasssa⒈把原本消亡的对象错误标记为存活;
    sasssa⒉把原本存活的对象错误标记为已消亡(很严重的错误);比如:
    sadssssa2.1赋值器插入了一条或多条从黑色对象到白色对象的新引用;
    sd asssa 2.2赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
    【注】:如果我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可:增量更新 和原始快照 。
  • 增量更新:破坏的是第一个条件: 当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
  • 原始快照:破坏的是第二个条件, 当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次 (其实可以理解为:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索)。
  • 无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用,譬如,CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。

G1中的火车算法你了解吗?
  • 火车算法也称列车算法,是一种更彻底的分区域处理收集算法,是对分代收集算法的一个有力补充。

  • 算法思路:
    1231 在火车算法中,内存被分为块,多个块组成一个集合。 为了形象化,一节车厢代表一个块,一列火车代表一个集合.火车与车箱都按创建顺序标号,每个车厢大小相等,但每个火车包含的车厢数不一定相等; 每节车箱有一个 记忆集合,而 每辆火车的记忆集合是它所有车厢记忆集合的总和;记忆集合由指向车箱中对象的引用组成,这些引用来自同一辆火车中序号较高的车箱中的对象,以及序号较高中的对象;

  • GC收集是 以车厢为单位,整体算法流程如下:
    1asdasd231⒈选择 标号最小的火车;
    1asdasd231⒉如果火车的 记忆集合是空的, 释放整列火车并终止, 否则进行第三步操作;
    1asdasd231⒊选择火车中 标号最小的车厢
    1asdasd231⒋对于 车厢记忆集合的每个元素进行判别
    1asdadasdsd1 ①、如果它是一个被根引用引用的对象, 那么, 将拷贝到一列新的火车中去;
    1asdadasdsd1 ②、如果是一个被其它火车的对象指向的对象, 那么, 将它拷贝到这个指向它的火车中去.;
    1asdadasds31 ③、假设有一些对象已经被保留下来了, 那么通过这些对象可以触及到的对象将会被拷贝到同一列火车中去;
    1asdaasss 231④、 如果一个对象被来自多个火车的对象引用, 那么它可以被拷贝到任意一个火车去;这个步骤中, 有必要对受影响的引用集合进行相应地更新;
    1asdasd231释放车厢并且终止;

  • 优点: 可以在成熟对象空间提供限定时间的渐近收集;而不需要每次都进行一个大区域的垃圾回收过程;即可以控制垃圾回收的时间,在 指定时间内进行一些小区域的回收;

  • 缺点: 实现较为复杂,如采用类似的算法的G1收集器在JDK7才实现;一些场景下可能性价比不高;

  • 应用场景: JDK7后HotSpot虚拟机 G1收集器采用类似的算法,能建立可预测的停顿时间模型


什么叫并发和并行?什么叫Minor GC 和 Major GC?

并行: 指多条垃圾收集线程并行工作,而此时用户线程仍然处于等待状态,如 ParNew、Parallel Scavenge、Parallel Old、G1;

并发: 指用户线程与垃圾收集线程同时/交替执行,如CMS、G1(也有并行);

Minor GC: 又称 新生代GC,指发生在新生代的GC动作;因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

Major GC: 又称 Full GC老年代GC,指发生在老年代的GC; 出现Major GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);Major GC速度一般比Minor GC慢10倍以上; 因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;


Serial 收集器

1a31Serial收集器是 最基础历史最悠久的收集器;

  • 特点:新生代、采用”标记-复制“算法、单线程工作的收集器(使用一个处理器或GC线程完成GC工作,必须STW)

  • 缺点: STW的时间长;

  • 优点:
    we⒈简单而高效(与其他收集器的单线程相比);
    we⒉额外内存消耗最小 (在内存资源受限的环境适用,比如近年流行的微服务应用中);
    we⒊单线程GC效率高(因为没有线程交互的开销,只专心做GC,适用单核处理器或者核心数较小的多核处理器);

  • 参数 -XX:+UseSerialGC: 添加该参数来显式的使用串行垃圾收集器;

  • 应用场景:HotSpot虚拟机运行在 客户端 模式下的默认新生代收集器;


SerialOld 收集器 (Serial GC器的老年代版本、CMS收集器发生失败时的后备预案)
  • 特点: ⒈老年代;⒉采用标记-整理算法 ⒊单线程收集;

  • 应用场景: wes主要用于主要用于 客户端模式; wes如果在服务端模式下,它也可能有两种用途:

JDK5以前,Serial Old ➕ Parallel Scavenge联合使用; 之后作为CMS收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure时使用;
sasdsddsdssdas


ParNew 收集器 (Serial收集器的 多线程并行版本)

1a31应用场景:在 Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

wes⒈-XX:+UseConcMarkSweepGC: 指定使用CMS后,会默认使用ParNew作为新生代收集器;
wes⒉-XX:+UseParNewGC: 强制指定使用ParNew;
wes⒊-XX:ParallelGCThreads: 指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

1a31ParNew➕Serial Old(JDK9废除)组合


Parallel Scavenge收集器 (吞吐量优先收集器)(Java8中,默认的垃圾收集器)

特点: 新生代收集器,基于 标记-复制算法实现的收集器,并行收集的多线程收集器,它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短GC时用户线程的停顿时间

【注】:吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值;
参数:
wes⒈-XX:MaxGCPauseMillis: 控制最大GC停顿时间,范围是大于0的毫秒数;
ws es注】:该参数不能无限小,因为垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的;
we s⒉-XX:GCTimeRatio: 直接设置吞吐量大小的,范围大于0小于100的整数;
w es⒊-XX:+UseAdaptiveSizePolicy: 这个参数被激活之后,就不需要人工指定新生代的大小、Eden与Survivor区域的比例、晋升老年代对象大小等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量;

应用场景:以高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互,比如批处理。


Parallel Old 收集器 (Parallel Scavenge收集器的 老年代版本)

Parallel Old是Parallel Scavenge收集器的 老年代版本,JDK 6时才开始提供;

特点: 老年代收集器、支持 多线程并行 收集、基于 标记-整理算法实现;

应用场景:JDK1.6及之后用来代替老年代的Serial Old收集器,特别是在Server模式,多CPU的情况下, 这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge➕Parallel Old收集器的"给力"应用组合

参数-XX:+UseParallelOldGC: 指定使用 Parallel Old收集器
asdasdsaasdasdsadasadadsadsad Parallel Scavenge➕Parallel Old收集器运行示意图如下
dassdsdddas在这里插入图片描述


CMS收集器 (以获取最短GC停顿时间为目标的收集器)(并发收集器)? 浮动垃圾问题?参数调试问题?并发收集带来的问题?

dss特点:老年代GC、基于" 标记-清除" 算法、

dss是HotSpot在JDK1.5推出的第一款 真正意义上的并发(Concurrent)收集器,第一次实现了让 GC线程与用户线程(基本上)同时工作

dss应用场景:目前很大一部分的Java应用集中 在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。

dss参数-XX:+UseConcMarkSweepGC: 指定使用CMS收集器;

dss整体分为四个步骤:sdasd①、初始标记 ②、并发标记 ③、重新标记 ④、并发清除

dss【注】: ① 和 ③ 两个步骤需要"STW"

ssaasd⒈初始标记: 仅只是 标记一下GC Roots能直接关联到的对象

ssaasd⒉并发标记: 从GC Roots的直接关联对象也就是①的基础上,开始 遍历整个对象图的过程,耗时长,但不需要停顿用户线程;
ssaasd⒊重新标记: 修正了并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,①<停顿时间<<③
ssaasd⒋并发清除: 清理标记阶段判断的已经死亡的对象由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时 并发的。
ssaasd总结: 由于在整个过程中耗时最长的 并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的GC回收过程是与用户线程一起并发执行的
asdasdsaasdasdsadasdsadasadadsadsad CMS收集器运行示意图如下:
dasddsdsasd在这里插入图片描述

  • 优点:并发收集、低停顿、也称之为“并发低停顿收集器”;
  • 缺点:
    ssaasdCMS收集器对处理器资源非常敏感(毕竟是面向并发设计的收集器)。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量;
    ssasdas[注]:CMS默认GC线程数量是(CPU数量+3)/4,当不足4个,性能不是很好;
    ssaasdCMS收集器无法处理“浮动垃圾”,甚至出现有可能出“Con-current Mode Failure”失败进而导致另一次完全“StopThe World”的 Full GC的产生。
    ssasds【浮动垃圾】: 在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    ssaasd基于“标记-清除”算法实现的收集器统一的缺点,也就是说着收集结束时会有 大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC的情况。
  • 并发收集带来的问题: 由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用(JDK5,CMS收集器当老年代使用了68%的空间后就会被激活,JDK6时该参数是92%)
  • 参数-XX:CMSInitiatingOccu-pancyFraction: 可以适当调高参数的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能
    【触发百分比过高带来的影响】: 要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了,所以这触发百分比需要权衡设计不能太高也不能过低。

Garbage First(G1)收集器?参数?调优?意义?

dssG1是一款面向 服务端应用的GC器,主要针对 配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼顾高吞吐量的性能
dss特点:
ssaasd⒈并行与并发:
ssasddsaasd①、并行性: G1在回收期间,可以有多个Gc线程同时工作,有效利用多核计算能力。此时用户线程STW
ssasddsaasd②、并发性: G1拥有与应用程序交替执行的能力 ,部分工作可以和应用程序同时执行,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
ssaasd⒉分代收集:
ssasddsaasd①、 能独立管理整个GC堆(新生代和老年代),而不需要与其他GC搭配;
ssasddsaasd②、能够 采用不同方式处理不同区域的对象,将整个堆划分为多个大小相等的独立区域 (Region),这些区域中包含了逻辑上的年轻代和老年代,它们不再要求一定是物理上连续的;
ssaasd⒊结合多种垃圾收集算法: 比如 空间整合不产生空间碎片从整体看,是基于"标记-整理"算法;从局部看,Region之间是是基于"标记-复制"算法;这是一种类似火车算法的实现;都不会产生内存碎片,有利于长时间运行;
ssaasd⒋可预测的停顿时间模型: G1除了追求低停顿处,还能建立可预测的停顿时间模型(这样低停顿的同时实现高吞吐量),可以明确指定M毫秒时间片内,GC消耗的时间不超过N毫秒;
【注】:JDK9以后,G1 是HotSpot的默认垃圾收集器,取代了CMS 回收器。
在这里插入图片描述
【上图分析】:
ssddsaasd①、所有的Region大小相同,且在JVM生命周期内不会被改变。 每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB。
ssddsaasd②、一个region只可能属于一个角色
ssddsaasd③、增加了一种新的内存区域,叫做Humongous内存区域, 如图中的H块。主要用于存储大对象,如果超过1.5个region,就放到H中。(对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题 ,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待
应用场景: ⒈面向 服务端应用,针对具有 大内存、多处理器的机器;⒉ 为 需要低GC延迟,并具有大堆的应用程序提供解决方案;

参数:
ssaasd⒈-XX:+UseG1GC: 指定使用G1收集器;
ssaasd⒉-XX:InitiatingHeapOccupancyPercent: 当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
ssaasd⒊-XX:MaxGCPauseMillis: 为G1设置暂停时间目标,默认值为200毫秒
ssaassd【注】: 如果这个值设置很小,如20ms,那么它收集的region会少,这样长时间后,堆内存会满。产生FullGC,FullGC会出现STW,反而影响用户体验;
ssaasd⒋-XX:G1HeapRegionSize: 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000;
ssaasd⒌ -XX:ParallelGCThread: 设置STW时GC线程数的值。最多设置为8GC线程);
ssaasd⒍-XX:ConcGCThreads: 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右;

调优操作步骤: ⒈开启G1垃圾收集器 ⒉设置堆的最大内存 ⒊设置最大的停顿时间

G1收集器运作过程(不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)(只有第二步没有STW):

ssaasd⒈初始标记: 仅只是 标记一下GC Roots能直接关联到的对象,且 修改TAMS(Next Top at Mark Start)来保证下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象(需要"STW",但耗时很短);
ssaasd⒉并发标记: 从GC Roots的直接关联对象也就是①的基础上,开始 遍历整个对象图的过程,耗时长,但不需要停顿用户线程(和CMS一样),
当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象
);
ssaasd⒊最终标记: 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。( 需要"STW",且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率);
ssaasd⒋筛选回收: 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须 暂停用户线程,由多条收集器线程 并行 完成的。
dsadsddsdsdas在这里插入图片描述

意义:
⒈G1收集器是垃圾收集器技术发展历史上的里程碑式的成果,开创了收集器面向 局部收集的设计思路基于Region的内存布局形式,并在JDK8提供了 并发的类卸载的支持,也称为“ 全功能的垃圾收集器”;
【为什么说是里程碑】:从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率,而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美.
⒉JDK 9版本,G1宣告取代Parallel Scavenge➕Parallel Old组合,成为 服务端模式下的默认垃圾收集器,而 CMS则沦落至被声明为不推荐使用(Deprecate)的收集器
⒊JDK 10版本,HotSpot虚拟机提出了“ 统一垃圾收集器接口”,将内存回收的“行为”与“实现”进行分离,CMS以及其他收集器都重构成基于这套接口的一种实现。 以此为基础,日后要移除或者加入某一款收集器,都会变得容易许多。


G1:在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?什么叫TAMS指针?位图的应用?

dsad 答:CMS收集器采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法来实现的。此外垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针并发回收时新分配的对象地址都必须要在这两个指针位置以上, G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。
【注】:与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop TheWorld”。

aasd在 GC 过程中新分配的对象都当做是活的,其他不可达的对象就是死的。如何知道哪些对象是 GC 开始之后新分配的呢?G1 在 Region 中通过 top-at-mark-start (TAMS) 指针来解决这个问题,分别使用 prevTAMS 和 nextTAMS 来记录新分配的对象。示意图如下:
aasdsdsdssdsdd在这里插入图片描述
aasd每个 Region 记录着两个 top-at-mark-start (TAMS) 指针,分别为 prevTAMS 和 nextTAMS。在 TAMS 以上的对象就是新分配的,因而被视为隐式 marked。
aasdG1 的 concurrent marking 用了两个 bitmap:
aasdsd一个 prevBitmap 记录 第 n-1 轮 concurrent marking 所得的对象存活状态。由于第 n-1 轮 concurrent marking 已经完成,所以这个 bitmap 的信息可以直接使用。
dsaasd一个 nextBitmap 记录 第 n 轮 concurrent marking 的结果。这个 bitmap 是当前将要或正在进行的 concurrent marking 的结果,尚未完成,所以还不能使用。
aasd其中 top 是该 Region 的当前分配指针,[bottom, top) 是当前该 Region 已用的部分,[top, end) 是尚未使用的可分配空间。

aasdsd①、[bottom, prevTAMS):这部分里的对象存活信息可以通过 prevBitmap 来得知。
aasdsd②、[prevTAMS, nextTAMS):这部分里的对象在第 n-1 轮 concurrent marking 是隐式存活的。
aasdsd③、[nextTAMS, top):这部分里的对象在第 n 轮 concurrent marking 是隐式存活的。


G1:为什么可以实现可预测的停顿(Region的妙用,以及优先列表)?如何做到可靠的呢(衰减均值)?

答:G1可以建立可预测的停顿时间模型是因为它可以有 是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。 更具体地处理思路是G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表; 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来),这就保证了在有限的时间内可以获取尽可能高的收集效率

可靠的解释: G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。


G1:跨Region引用对象的问题?(记忆集,写屏障)

答:无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描, 每个Region都有一个Remembered Set;每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作; 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象); 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。
在这里插入图片描述
【另一种说法】:使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号 (这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)),这种结构比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。


G1:筛选回收为什么没有设计成与用户程序一起执行并发执行?

ssd其实也考虑让其一起并发执行,但是这种操作比较复杂,并且我们回收的不是所有的Region,而是一部分,因此停顿时间是可控的;还有就是G1不仅仅是面对低时延,还需要保证吞吐量,而停顿用户线程能最大程度的提高GC效率,因此我们把这个操作放到了ZGC中实现。
sd【注】: G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程;


G1与CMS的比较?

G1比CMS性能好的情况: ⒈超过50%的Java堆被占用;⒉对象分配频率或年代提升频率变化很大;⒊GC停顿时间过长;

ssdG1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。

G1比CMS差的情况:
ssaad内存占用方面: 是因为G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间,并且相比其他收集器,除了记忆集(“卡表”)还有Collection Sets的损耗:将要被回收的小堆区集合。GC 时,在这些小堆区中的对象会被复制到其他的小堆区中,总体上 Collection Sets 消耗的内存小于 1%。

ssaad执行负载方面: 虽然它们都会用到写屏障,但CMS只是用写后屏障来更新维护卡表,而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在整个标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理


G1 :回收过程可以说说吗 ?
  • G1 的GC过程主要包括三个环节:
    sad⒈年轻代GC(Young GC)ssd⒉年轻代GC➕老年代并发标记过程(Concurrent Marking)sad⒊混合回收(Mixed GC)
    dsadsdsdsdsddsasd在这里插入图片描述
  • 什么时候开始年轻代GC?
    年轻代的Eden区快用尽时开始年轻代的GC过程,G1的年轻代收集阶段是一个 并行(多个GC线程)的独占式收集器组成。在年轻代回收期,G1 GC 暂停所有应用程序线程,启动多线程执行年轻代回收。然后 从年轻代区间移动存活对象到Survivor区间或者老年区间, 也有可能是两个区间都会涉及。
  • 什么时候开始年轻代GC+老年代并发标记?
    堆内存使用达到一定值(默认45%)时, 开始老年代并发标记过程;
  • 什么时候开始混合回收?
    老年代标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。 和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收 ,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个 老年代Region是和年轻代一起被回收的

年轻代GC详解

  • 当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程,来回收 Eden区和 Survivor区:
    asdasdsaasdasdsadasadadsadsad 年轻代GC前后对比图如下

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

  • ①、根扫描: 一定要考虑 remembered Set,看是否有老年代中的对象引用了新生代对象;
    ②、更新RSet:处理dirty card queue中的card,更新RSet。 此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用
    【dirty card queue】:dirty card queue: 对于应用程序的引用赋值语句object.field=object,JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会对Dirty CardQueue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多;
    ③、处理RSet: 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象;
    ④、复制对象:"标记-复制"算法,此阶段,对象图被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1达到阀值会被会被复制到old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间;
    ⑤、处理引用: 处理Soft,Weak, Phantom, Final, JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而 目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片

并发标记过程详解

①. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC;
②.根区域扫描:G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成(YoungGC时,因为会动Survivor区,所以这一过程必须在young GC之前完成)
③.并发标记 : 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
④.再次标记: 由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的原始快照算法:snapshot一at一the一beginning (SATB);
⑤.独占清理: 计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。(这个阶段并不会实际上去做垃圾的收集)
⑥.并发清理阶段: 识别并清理完全空闲的区域

混合回收

  • Mixed GC并不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区, 正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC(网图,如下)
    在这里插入图片描述

Full GC

  • 堆内存过小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到full gc,这种情况可以通过增大内存解决;
  • 暂停时间-XX:MaxGCPauseMillis设置短,回收频繁。由于用户线程和GC线程一起执行,可能用户线程产生的垃圾大于GC线程回收的垃圾,会导致内存不足,触发Full gc;

7种经典垃圾回收器总结

**G1比CMS性能好的情况:** ⒈超过50%的Jav


怎么选择GC器? (JVM优化)

sdada①、优先调整堆的大小让JVM自适应完成;
sdada②、如果 内存小于100M,使用串行GC器;
sdada③、如果是单核、单机程序,并且没有停顿时间的要求,选择串行GC器;
sdada④、如果是 多CPU、需要高吞吐量、 允许停顿时间超过1秒,选择并行或者JVM自己选择;
sdada⑤、如果是 多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。


谈一谈GC的新型GC回收器
衡量GC器性能的三项最重要的指标有什么?

asda这就涉及到了衡量GC器性能的三项最重要的指标:①、内存占用②、吞吐量 ③、低时延

sdad随着计算机硬件的发展,其内存大了,性能好了,我们对内存的要求也放宽了限制(可以容忍GC器多占用一点内存),自然吞吐量也上去了(因为性能好了)。不过内存大了那么时延必定相应增大了。

①、Serial,Parallel : 回收过程的所有步骤都要挂起用户线程,也就是STW,随着内存的增大,堆自然增大,那么停顿时间也会随之增长
②、CMS: 使用 增量更新技术,实现了标记阶段的并发,不会随着堆的增大停顿时间也增大。但是对于标记之后的回收处理,并不是很好:标记之后通过 “标记-清除” 算法虽然也可以做到并发,但是会产生空间碎片,早晚也要进行 Full GC
③、G1: 使用 原始快照技术,实现了标记阶段的并发,不会随着堆的增大停顿时间也增大。但是对于标记之后的回收处理,同样有缺陷:虽然可以按更小的粒度Region进行回收,从而降低了时延,但是随着堆的增大,时延也会增大
④、Shenandoah,ZGC: 几乎整个工作过程全部都是 并发的,只有初始标记、最终标记这些阶段有短暂的停顿,而这部分 停顿的时间基本上是固定的,与堆的容量、堆中对象的数量 没有正比例关系 不过这两款GC器目前都在 实验阶段(我没调查这两年是不是还处于这个阶段)。但是这两款都可以称为 低时延GC器。 (回收的改进)


谈一谈Shenandoah收集器和G1的不同?(最大点:二阶矩阵替代记忆集,并不使用分代,以及并发)

sdad⒈相同点: Shenandoah和G1收集器都有着 相似的堆内存布局,在初始标记、并发标记等许多阶段的处理思路上都是一样的,并共享了一部分实现代码,比如它们都有在并发失败后作为“逃生门”的Full GC,都是使用基于Region的堆内存布局,同样有着用于存放大对象的HumongousRegion,默认的回收策略也同样是优先处理回收价值最大的Region等等
sdad⒉不同点:
asaa①、Shenandoah支持 并发的“复制-整理”
asaa②、Shenandoah默认 不使用分代收集,也就是说不会有新生代,老生代;
asaa③、Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的 记忆集,改用名为“ 连接矩阵”(Connection Matrix)的全局数据结构来 记录跨Region的引用关系降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。
【连接矩阵的使用】:连接矩阵可以理解为一个二阶矩阵,如果Region N有对象指向Region M,就在矩阵的N行M列处做标记,如图。
asdsdsdsdssaa在这里插入图片描述


Shenandoah收集器的工作过程可以说一说吗?

在这里插入图片描述


Shenandoah收集器的转发指针、读屏障、引用访问屏障 说一说?
  • Brooks Pointer : “Brooks”是一个人的名字。1984提出了使用 转发指针(Forwarding Pointer,也常被称为 Indirection Pointer)来 实现对象移动与用户程序并发的一种解决方案
  • 在这之前,我们要实现类似的并发操作的大致思路如下:
    sdaasdsa①、通常是在被移动对象 原有的内存上设置保护陷阱
    sdaasdsa②、一旦用户程序访问到归属于旧对象的内存空间就会产生 自陷中段,进入预设好的 异常处理器中, 再由其中的代码逻辑把访问转发到 复制后的新对象上。
    sdaasds 【缺点】: 虽然确实能够实现对象移动与用户线程并发但是如果没有操作系统层面的直接支持,这种方案将导致用户态频繁切换到核心态,代价是非常大的,不能频繁使用。
  • 如今的新方案: 不需要用到内存保护陷阱,而是在原有对象布局结构的最前面统一增加一个新的引用字段 ,如图:
    在这里插入图片描述
    【转发指针的性能分析】:
    sddsa⒈从结构上看,转发指针早期JVM使用的句柄定位比较像:两者都是一种 间接性的对象访问方式,差别是句柄通常会 统一存储在专门的句柄池中,而转发指针是 分散存放在每一个对象头前面
    sddsa⒉既然都是间接性的对象访问方式,那么就有特有的 缺点:每次对象访问会带来一次额外的转向开销,尽管这个开销已经被优化到只有一行汇编指令的程度。
    sddsa⒊虽然这是一比不小的开销,但是 对于内存保护陷阱方案已经有了很大的改善: 因为当我们复制对象时,也就是对象有了副本,我们仅仅需要修改一处指针的值,也就是旧对象上转发指针的引用位置,使其指向副本,也就是指向新的对象,这样便可将所有对该对象的访问转发到了新的副本上(这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然可用,都会被自动转发到新对象上继续工作)。
    在这里插入图片描述
    【转发指针的相关操作所需要的一些注意事项】:
    sddsa对转发指针的访问操作采取同步措施: 让收集器线程或者用户线程对转发指针的访问只有其中之一能够成功,另外一个必须等待,避免两者交替进行; 而我们可以通过 CAS 操作保证这种行为。
    sddsa执行频率问题: 尽管 通过对象头上的Brooks Pointer来保证并发时原对象与复制对象的访问一致性,但是这个对象的访问其实是很复杂的:包括对象的 读取、写入,对象的比较,为对象哈希值计算,对象加锁等;而我们要覆盖全部对象访问操作,Shenandoah不得不同时设置 读、写屏障 去拦截。
    【读、写屏障的一些问题】:
    sddsa写屏障: 在上一篇文章的介绍七种经典回收器中, 无论是为了维护卡表,还是用于实现并发标记,写屏障已被使用多次,累积了不少的处理任务了,这些写屏障有相当一部分在Shenandoah收集器中依然要被使用到。
    sddsa读屏障: 为了实现 Brooks Pointer, Shenandoah在读、写屏障中都加入了额外的转发处理,不过代码里对象读取的出现频率要比对象写入的频率高出很多,读屏障数量自然也要比写屏障多得多,所以读屏障的使用必须更加谨慎,不允许任何的 重量级操作Shenandoah是我们所分析的所有回收机中第一款使用到读屏障的收集器,
    sddsa读屏障的改进: JDK 13中将Shenandoah的内存屏障模型改进为 基于引用访问屏障 的实现,所谓 “引用访问屏障”是指内存屏障只拦截对象中数据类型为引用类型的读写操作,而不去管原生数据类型等其他非引用字段的读写,这能够省去大量对原生类型、对象比较、对象加锁等场景中设置内存屏障所带来的消耗。
    【Shenandoah收集器的强弱项】:
    sddsa强项: (低延迟时间)建立量化的概念; sddsa弱项: 高运行负担使得吞吐量下降;
    【总结】: Shenandoah收集器中用到的新的技术:Brooks Pointer转发指针、读屏障(引用访问屏障)

谈一谈ZGC收集器 ?
  • ZGC 是一款在JDK 11中新加入的具有实验性质的 低延迟垃圾收集器,是由Oracle公司研发的;

  • 目标: ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟

  • 主要特征: GC采用 基于Region的堆内存布局,(暂时)不设分代的使用了 读屏障染色指针内存多重映射等技术来实现 可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器

  • 【Region的差异】: ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本文为行文一致继续称为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的 Region可以具有大、中、小三类容量:
    sdsdsa① 小型Region: 容量固定为2MB,用于放置对象≤256KB;
    sdsdsa② 中型Region: 容量固定为32MB,用于放置256KB≤对象<4MB的对象;
    sdsdsa③ 大型Region: 容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置 4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到) 的,因为复制一个大对象的代价非常高昂。
    sadadsdasdasdasd在这里插入图片描述

  • ZGC的运作过程大致可划分为以下四个大的阶段(都是并发过程):
    在这里插入图片描述

  • ①、并发标记(Concurrent Mark):
    sddsa⒈初始标记(Mark Start): 先STW,并记录下 GC Roots直接引用的对象
    sddsa⒉并发标记(Concurrent Mark): 根据初始标记的结果,基于GC Roots可达性分析算法找出所有被引用的对象,在 G1、Shenandoah中使用 BitMap 的结构来记录三色标记信息ZGC使用 染色指针 做标记
    s dsa ⒊最终标记(Mark End): 先STW,然后修复一些在并发标记过程中垃圾状态出现变化的对象。

  • ②、并发预备重分配(Concurrent Prepare for Relocate): 这个阶段ZGC会根据特定的查询条件扫描一下所有的Region并得出本次收集过程中 需要清理哪些Region,将它们重新组成 重分配集(Relocation Set)用范围更大的扫描成本换取省去G1中记忆集的维护成本。

  • ③、并发重分配(Concurrent Relocate):
    sddsa⒈初始重分配(Relocate Start): 做一些并发重分配的初始化动作。
    sddsa⒉并发标记(Concurrent Mark): 这个阶段需要将并发预备重分配阶段计算出来的重分配集中的Region复制到新的Region并为每一个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系,从转发表ZGC就可以明确的知道哪些对象是否处于重分配集之中,在这个阶段时,如果有用户线程访问这个对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后根据Region的转发表找出新的地址并访问,如果有更新在更新地址上的值,并使其指向新对象(这样子只有第一次访问时会变慢,后面的就可以不通过读屏障和转发表直接访问),ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。 (转发表的功能类似转发指针)
    sddsa【注】: 一旦一个Region中的对象全部复制完成,旧的Region就可以清理释放掉了,但是转发表不能立即释放,因为可能还有访问在使用这个转发表,因为对象的旧地址转新地址是对象在被引用之后才会进行的操作。

  • ④、并发重映射(Concurrent Remap): 重映射其实就是将旧的地址转换为新的地址,由于ZGC中对象引用存在“自愈”功能,所以这个阶段其实不做也是可以的,ZGC很巧妙的将这一阶段合并到了下一次的并发标记阶段,反正他们都是要遍历所有对象的,这样也就减少了一次遍历对象的开销,一个Region的所有对象都被修改后,那么这个Region对应的转发表就会被销毁掉。


ZGC :引入的新技术有什么(染色指针技术)?
  • Shenandoah使用 转发指针读屏障来实现并发整理;而ZGC更加巧妙,那就是 染色指针技术来替代转发指针
  • 在上文我们已经说过对象访问的相关操作,如果我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据通常会在对象头中增加额外的存储字段,如对象的哈希码、分代年龄、锁记录等就是这样存储的。 当这种在 对象头记录信息的方式,在有对象访问的情况下自然是很好的一种选择,但是如果这个对象被移动了呢?如果我们只是 单纯了解某些信息(比如对象是否移动过,比如对象是否被引用(三色标记),而不是访问对象呢?那么我们就要考虑别的记录信息的方式,比如通过指针或者与内存无关的地方得到这些信息。
  • 我们接下来看一下HotSpot虚拟机不同的收集器的不同的标记实现方式:
    sddsa⒈Serial收集器:把 标记直接记录在对象头上;
    sddsa⒉G1、Shenandoah:把 标记记录在与对象相互独立的数据结构上(一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息);
    sddsa3 ZGC:染色指针技术,是最直接的、最纯粹的,它直接把标记信息记在引用对象的指针上,这时,与其说可达性分析是遍历对象图来标记对象,还不如说是 遍历“引用图”来标记“引用”了
  • 染色体技术到底是什么?
    染色指针是一种直接将少量额外的信息存储在指针上的技术;
  • 指针怎么存储的呢?
    sddsa不同的操作系统采取不同的存储方式,大致都一样,只不过位数不一样。比如在64位系统中,理论上可以存储2^64的字节,不过实际上是做不到的,当然也用不到那么多内存(因为位数越长,在做地址转换的时候需要的页表级数越多,成本也更高)比如:
    sddsa64位的Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间
    sddsa64位的Windows系统只支持44位(16TB)的物理地址空间。
    sddsa尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到。当然,由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致 ZGC能够管理的内存不可以超过4TB(2^42);
    在这里插入图片描述

ZGC :染色指针技术带来的性能的提高(与SChenandoah的判断)?

sddsa⒈染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。 这点相比起Shenandoah是一个颇大的优势,使得理论上只要还有一个空闲Region,ZGC就能完成收集,而Shenandoah需要等到引用更新阶段结束以后才能释放回收集中的Region,这意味着堆中几乎所有对象都存活的极端情况,需要1∶1复制对象到新Region的话,就必须要有一半的空闲Region来完成收集。
sddsa染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。实际上,到目前为止ZGC都并未使用任何写屏障(这也说明ZGC对吞吐量影响很小),只使用了读屏障(一部分是染色指针的功劳,一部分是ZGC现在还不支持分代收集天然就没有跨代引用的问题
sddsa[注]: 内存屏障(Memory Barrier)的目的是为了指令不因编译优化、CPU执行优化等原因而导致乱序执行,它也是可以细分为仅确保读操作顺序正确性和仅确保写操作顺序正确性的内存屏障的。
sddsa3 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。 譬如存储一些追踪信息来让垃圾收集器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。


ZGC :染色体技术带来的问题?

染色体指针技术带来的问题:Java虚拟机作为一个普普通通的进程, 这样随意重新定义内存中某些指针的其中几位,操作系统是否支持?处理器是否支持(多重映射技术)?
sddsa无论中间过程如何,程序代码最终都要转换为 机器指令流 交付给处理器去执行,处理器可不会管指令流中的指针哪部分存的是标志位,哪部分才是真正的寻址地址,只会把整个指针都视作一个内存地址来对待。

sddsaZGC设计者就只能采取其他的补救措施了,这里的解决方案涉及 虚拟内存映射技术。

sddsaLinux/x86-64平台上的ZGC使用了 多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射,意味着ZGC在虚拟内存中看到的地址空间要比实际的堆内存容量来得更大。把染色指针中的标志位看作是地址的分段符,那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了。
dsdsdssddsadas在这里插入图片描述


相比G1、Shenandoah,ZGC的在分代方面的权衡
  • 相比 G1、Shenandoah等先进的垃圾收集器,ZGC在实现细节上做了一些不同的权衡选择:
    sda⒈ 比如,G1 需要通过 写屏障来维护 记忆集,才能处理 跨代指针,得以 实现Region的增量回收。而记忆集要占用大量的内存空间,写屏障也对正常程序运行造成额外负担,这些都是权衡选择的代价。
    sdd⒉ ZGC就 完全没有使用记忆集,它甚至 连分代都没有,连像CMS中那样只记录新生代和老年代间引用的 卡表也不需要因而完全没有用到写屏障,所以给用户线程带来的运行负担也要小得多。
  • 可是,必定要有优有劣才会称作权衡,ZGC的这种选择也限制了它能承受的对象分配速率不会太高
    【可以想象以下场景来理解 ZGC的这个劣势】: ZGC准备要对一个很大的堆做一次完整的并发收集,假设其全过程要持续十分钟以上(切勿混淆并发时间与停顿时间,ZGC立的Flag是停顿时间不超过十毫秒),在这段时间里面,由于应用的对象分配速率很高,将创造大量的新对象,这些新对象很难进入当次收集的标记范围,通常就只能全部当作存活对象来看待——尽管其中绝大部分对象都是朝生夕灭的,这就产生了大量的 浮动垃圾。如果这种高速分配持续维持的话,每一次完整的并发收集周期都会很长,回收到的内存空间持续小于期间并发产生的浮动垃圾所占的空间,堆中剩余可腾挪的空间就越来越小了。
    [劣势如何解决呢?] :目前唯一的办法就是 尽可能地增加堆容量大小,获得更多喘息的时间。但是若要从根本上提升ZGC能够应对的对象分配速率,还是需要 引入分代收集,让新生对象都在一个专门的区域中创建, 然后专门针对这 个区域进行更频繁、更快的收集

System.gc() 的理解

sddsa1、在默认情况下,手动调用System.gc()或者RunTime.getRunTime().gc(),会显式出发 FullGC 同时对新生代和老年代进行回收,尝试释放垃圾。
sddsa2、然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(无法保证马上触发GC)。

sddsa3、JVM实现者可以通过system.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。


JVM之类文件结构
JAVA(JVM)的"平台无关性"真的做到了吗

dsad 其实我们可以说成是"伪平台无关性",因为它只是在操作系统以上的应用层实现了平台无关性,并且还是要借助于 虚拟机和字节码
dsad Oracle公司以及其他VM发行商发布过 许多可以运行在各种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写,到处运行”。
dsad 因此我们可以说,字节码是构成"平台无关性"的基石


可以说JAVA(JVM)实现了"语言无关性"吗?

dsad 其实到现在可以说实现了"语言无关性",因为在JAVA技术刚发展的时候,设计者就已经考虑让JVM执行其他语言的可能。并在2018年,基于HotSpot扩展而来的GraalVM公开之后,基本实现。
dsad 实现"语言无关性"的基础仍然是虚拟机 字节码 存储格式。


JVM具体怎么实现"语言无关性"呢?

dsadJVM不与任何程序语言绑定。它只与“Class文件”这种特定的二进制文件格式所关联, Class文件中包含了Java虚拟机指令集、符号表等各种辅助信息。
dsad作为一个通用的、与机器无关的执行平台,任何其他语言的实现者都可以将 Java虚拟机作为他们语言的运行基础,以 Class文件作为他们产品的交付媒介。


谈一谈class文件的注意事项?

dsad①、Java具有良好的 向后兼容性(兼容老版本)
dsad②、任何一个Class文件都对应着唯一的一个类或接口的定义信息(这句话不太准确,比如package-info.class、module-info.class这些文件就属于完全描述性的。),但是反过来则不一定,类或接口并不一定都得定义在Class文件里(比如类或接口也可以 动态生成直接送入类加载器中
dsad③、Class文件是一组 8个字节为基础单位的二进制流,中间没有添加任何分隔符。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成多个8个字节进行存储。
dsad④、Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:
dsasdaaaasasasassasasdasdsdad㈠、无符号数dsdsdad ㈡、表
dsaasdad无符号数: 基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。
dsaasdad表:由 多个无符号数➕其他表 作为数据项构成的复合数据类型。所有表的命名都习惯性地以“_info”结尾。表用于描述 有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。


class文件格式?

dsadas在这里插入图片描述
dsad1、魔数 : 确定这个文件是否是一个能被虚拟机的Class文件

dsad2、副版本号 :标识“技术预览版”功能特性

dsad3、常量池容量计数:常量池中常量的数量是不固定的,记录从1开始,比如22对应21项常量,class文件中只有常量池的容量计数从1开始。那为什么设计者将第0项常量空出来呢? 因为如果后面某些指向常量池索引值的数据在某些情况下,想要表达"不引入任何一个常量池的数据",则可以把这个索引值设置为0;
dsdsad常量池中主要存放两大类常量:㈠、字面量 ㈡、符号引用。前者很接近Java语言对于常量的定义(字符串、被final修饰的常量等),后者则倾向于编译原理方面,主要包括:
dsdsad被模块导出或者开放的 包(Package)dsad类和接口的全限定名(Fully Qualified Name)
dsdsad字段的名称和描述符(Descriptor)dsad方法的名称和描述符dsad方法句柄和 方法类型dsad动态调用点和动态常量


谈一谈什么叫动态链接?

dsadJava代码在进行Javac编译的时候,并不像C和C++那样有“连接”这 一步骤,而是在虚拟机加载Class文件的时候进行动态连接,也就是说在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址(也就是说都是间接引用,不是直接引用),也就无法直接被虚拟机使用。当虚拟机做 类加载时,将会从常量池获得对应的 符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中(解析阶段:间接引用->直接引用的过程)。


dsdsad常量池的每一个常量都是一个表 ,表结构起始的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种常量类型

dsad4、访问标志:(用于识别一些 类或者接口层次的访问信息,包括:这个Class是 类还是接口;是否定义为 public类型;是否定义为 abstract类型;如果是类的话,是否被声明 为final;等等)

dsad5、类索引、父类索引与接口索引集合: 类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来 确定该类型的继承关系类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合就用来描述这个类实现了哪些接口。

dsad6、字段表集合 :主要包括:类级变量、实例级变量、 不包括 :局部变量

dsdsad字段可以包括的修饰符:字段的作用域、实例变量还是类变量、可变性、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能 引用常量池中的常量来描述。


什么叫全限定名,简单名称,描述符?

dsasdaad⒈全限定名: 比如org/fenixsoft/clazz/TestClass ,一般以";"结尾。
dsasdaad⒉简单名称: 指没有类型和参数修饰的方法或者字段名称,比如inc()方法m字段的简单名称分别就是“inc”和“m”。
dsasdaad⒊描述符: 用来描述字段的数据类型、方法的 参数列表(包括数量、类型以及顺序)返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示;
【注】:描述符举例:
dsasdaad⒈对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[] []”类型的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。
dsasdaad⒉用描述符来描述方法时,按照 先参数列表、后返回值 的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”
【注】: 字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称。但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就是合法的。


dsad7、方法表集合 :Class文件中 对方法的描述与对字段的描述采用了几乎完全一致。依次包括访问标志、名称索引、描述符索引、属性表集合几项。

方法里的代码编译后放在哪里?

dsad 经过Javac编译器编译成字节码之后,会存放在方法的属性表集合中一个名为“Code”的属性里面。

字节码层面父类方法没有被重写,子类方法集合中会有父类的方法信息吗?

与字段表集合相似,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。 但同样地,有可能会出现由编译器自动添加的方法。比如:类构造器“< clinit >()”方法、实例构造器“< init >()”方法。

对于重载的判断?特征签名是什么呢?方法返回值不同可以当作重载吗?

dsad⒈ 在Java语言中,要重载(Overload)一个方法,要满足两个条件:
sdadsda①、具有相同的简单名称;②、必须拥有一个
与原方法不同的特征签名

dsad⒉什么是特征签名呢?
sdadsda特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合也正是因为返回值不会包含在特征签名之中,所以Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。
sdadsda但是在Class文件格式之中,特征签名的范围要更大一些,只要描述符不是完全一致的两个方法就可以共存。
sdadsda举例:如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。
【总结】:
sdadsda⒈Java代码的方法特征签名包括:方法名称➕参数顺序➕参数类型;
sdadsda⒉字节码的特征签名包括:Java代码的方法特征签名➕方法返回值➕异常表;


dsad8、属性表集合 :

谈一谈Code属性?this的位置?异常表?
  • Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。只不过不是所有的方法都包含这个属性,比如:接口或者抽象类中的方法就没有Code属性。
  • max_stack: 表示操作数栈的最大深度。
    max_locals: 表示局部变量表所需的存储空间。单位:变量槽(Slot);
    【注】:
    saddsa槽是VM为局部变量分配内存所使用的最小单位。 比如,方法参数(别忘了this)➕ 显式异常处理器的参数(就是try-catch块中catch所定义的异常)➕方法体中定义的局部变量。它们都需要局部变量表来存放。
    saddsa注意,由于局部变量表中的slot可以重用,所以并不是所有的局部变量的总slot就是max_locals。编译器会根据变量的作用域来分配slot给不同的变量使用,然后计算max_locals的大小。
    code_length和code: 存储字节码指令。Java的字节码指令的长度为一个字节。理论上最多可以有256个指令,不过目前只用了200多个指令。

dad⒉在任何实例方法(静态方法不会有this) 里面,都可以通过“this”关键字来访问此方法所属的对象。它的原理是通过 在编译器编译的时候把对this关键字的访问转换成对一个普通方法参数的访问,然后在虚拟机调用实例方法时会自动传入此参数。 所以在实例方法的局部变量表中的第一个槽位都会用来存放对当前对象实例的引用this。

dad3、异常表其实是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常即finally处理机制。因此,finally中的内容会在try或catch中的return语句之前执行,并且在try或catch跳转到finally之前,会将其内部需要返回的变量的值复制一份副本到本地变量表的最后一个槽中

谈一谈ConstantValue属性?static和final修饰?
  • 让虚拟机自动为 静态变量(只有静态变量才可) 赋值;
  • 虚拟机对变量的赋值方式有两种
    sad⒈对于实例变量,在 实例构造器< init >() 方法中赋值;
    sad⒉对于类变量,可以在类构造器< clinit >() 方法或使用 ConstantValue赋值;
  • Oracle公司的选择:
    sad⒈使用 ConstantValue进行初始化要满足两个条件:①、同时使用final和static来修饰一个变量 ②、这个变量是基本数据类型或者java.lang.String。 (准备阶段,赋的不是默认值,是具体值)
    sad⒉选择< clinit >()方法进行初始化满足其中一个条件即可:①、该变量没有被finale修饰 ②、该变量不是基本数据类型及字符串。 (初始化阶段)(只要求有 ConstantValue属性的字段必须设置ACC_STATIC标志而已,对final关键字的要求是Javac编译器自己加入的限制。)
谈一谈Signature属性 (反射获得泛型信息)?
  • 是在JDK5加入到Class文件规范中的;
  • 任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息。
  • 为什么要专门用一个属性记录泛型类型呢? 主要是因为Java语言的泛型采用的是 擦除法实现的 伪泛型,字节码(Code属性)中 所有的泛型信息编译(类型变量、参数化类型)在编译之后都通通被擦除掉 。
    【注】:
    sad⒈擦除法的好处: 实现简单,运行期也能够节省一些类型所占的内存空间。
    sad⒉擦除法的坏处: 运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待。
    sad比如:运行期做反射时无法获得泛型信息。 Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取的泛型类型,最终的数据来源也是这个属性。
谈一谈 MethodParameters 属性?(反射获得形参信息,LocalVariableTable)
  • 在JDK 8时新加入到Class文件格式中;
  • 记录方法的各个形参名称和信息。
  • 其实一开始由于存储空间比较小,所以Class文件默认不存储方法参数名称的,但是对于程序的传播和二次复用会有不小的影响,在JDK8之前可以将方法参数的名称生成到LocalVariableTable中。但是它有其局限性:LocalVariableTable属性是Code属性的一个子属性,它没有方法体的存在,更不会有局部变量表,总之就是没有一个可以完整保存方法参数名称的地方。
  • 基于LocalVariableTable的局限性,增加了MethodParameters属性,这样可以在编译的时候,通过加上parameters参数,将方法名称也写入到Class文件中。最主要的是,MethodParameters是方法表的属性,与Code属性是平级的,可以在运行的时候通过反射API获取。
谈一谈 RuntimeVisibleAnnotations属性?(反射获得注解)

sadRuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或 方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字 段或方法上的注解时,返回值就是通过这个属性来取到的。


虚拟机的指令?

dsadJava虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。
dsad由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。 因此我们可以粗略认为字节码指令就由操作码组成,长度为一个字节。


字节码指令集的操作码总数一共有多少个呢?

dsad 由于JVM操作码的长度为1个字节,所以指令集的操作码总数≤256条。
dsa d意味着JVM在处理超过一个字节的数据时,不得不在运行时从字节中重建出具体的数据结构,比如要将一个16位长度的无符号整数使用两个两个无符号字节(byte1 、byte2)存储起来:可以表示成 (byte1 << 8) | byte2;
【注】:因为Java虚拟机的操作码长度只有一字节,所以我们不可能让指令集相互独立,其实也就是说不可能让每一种数据类型和每一种操作都有对应的操作码指令。 比如在某种情况下,我们可以通过一些指令将一些不支持的数据类型转换成为可被支持的数据类型。


以这种方式解释执行(操作数放在操作数栈中,只使用操作码)字节码有什么优缺点呢?

dsad缺点:解释执行字节码要损失一些性能。
dsad优点:我们不用担心操作数长度对其问题,毕竟只用操作码表示字节码操作,并且长度都是一个字节。当然凡事无绝对,虽然字节码指令流基本上都是单字节对齐的,但是有“tableswitch”和“lookupswitch”两条指令例外,由于它们的操作数比较特殊,是以4字节为界划分开的,所以这两条指令需要预留出相应的空位填充来实现对齐。

dsd 它的基本执行模型(不考虑异常)如下:

do {
	自动计算PC寄存器的值加1;
	根据PC寄存器指示的位置,从字节码流中取出操作码;
	if (字节码存在操作数) 从字节码流中取出操作数;
	执行操作码所定义的操作;
} while (字节码流长度 > 0);

窄化类型转换规则?

·1asd23Ⅰ、如果浮点值是NaN,那转换结果就是int或long类型的0。
·1asd23Ⅱ、如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v。如果v在目标类型T(int或long)的表示范围之类,那转换结果就是v;否则,将根据v的符号,转换为T所能表示的最大或者最小正数。
·1asd23Ⅲ、double—>float转换时,如果换算后的绝对值太小而不能使用float来表示的时候,就返回float类型的正负0,如果换算后的值太大,将返回float类型的正负无穷大。而double的NAN值将转换为float的NAN值。
·1asd23Ⅳ、窄化转换指令发生的精度丢失以及溢出都不会导致JVM抛出运行时异常。


控制转移指令的比较?(byte、char、short、boolean的比较)

对于boolean类型、byte类型、char类型和short类型的条件分支比较操作,都使用int类型的比较指令来完成。而对于long类型、float类型和double类型的条件分支比较操作,则会先执行相应类型的比较运算指令(dcmpgdcmplfcmpgfcmpllcmp);
【注】:由于各种类型的比较最终都会转化为int类型的比较操作,int类型比较是否方便、完善就显得尤为重要,而JVM提供的int类型的条件分支指令是最为丰富、强大的。


方法调用和返回指令?
  • invokevirtual指令: 用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派);
  • invokeinterface指令: 用于调用接口方法;
  • invokespecial指令: 用于调用一些需要特殊处理的实例方法,包括实例初始化方法私有方法父类方法
  • invokestatic指令: 用于调用类静态方法(static方法)
  • invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
    【总结】: 前面四条指令的分派逻辑都固定在JVM内部,用户改变不了,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 方法调用指令与数据类型无关
  • 方法返回指令:
    1231sa23包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。

异常处理指令?
  • 抛出异常指令:athrow
  • 除了用throw语句显式抛出异常的情况之外,《Java虚拟机规范》还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出
  • 注意:在JVM中,处理异常(catch语句)不是由字节码(原来由jsr,ret指令实现)来实现的,而是异常表来实现的。

同步指令 ?
  • JVM支持方法级的同步和方法内部一段代码的同步,它们都是使用管程(Monitor,也可以称为"锁") 来实现的。

  • 方法级的同步是 隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。JVM可以通过常量池中的方法表结构中的 ACC_SYNCHRONIZED访问标志得知该方法是否是同步的:
    1a23如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放 管程
    1a23在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。

  • Java虚拟机的指令集中有 monitorentermonitorexit两条指令来支持 synchronized关键字的语义。

  • 注意:
    a23Ⅰ、方法中调用过的每条monitorenter指令都必须有其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。
    a23Ⅱ、我们为了保证程序出现异常时,仍能使monitorenter指令和monitorexit指令正确配对执行,片一起会自动产生一个异常处理程序,它的目的就是用来执行monitorexit指令。


Class文件的平台中立性?

dsadClass文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱。


虚拟机字节码执行引擎

栈帧包括什么?

sdasd每一个栈帧包括:局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
sdasd在编译时期,栈帧需要多大的局部变量表,多深的操作数栈就已经被计算出来,并写入到了方法表的Code属性中。
sdasd总之,一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式(可能有重叠部分)


当前栈帧指什么?

sdasd以Java程序的角度来看:同一时刻、同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。
sdasd对于执行引擎来讲:在活动线程中,只有位于栈顶的方法才是运行的,可以生效的,被称为"当前栈帧"。
dsadadasdasddsas在这里插入图片描述


局部变量表是什么?

sdasd局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
sdasd在编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量
sdasd局部变量表的容量:以变量槽(Variable Slot) 为最小单位。
sdasdJVM通过索引定位的方式使用局部变量表,索引值的范围是从0开始。但是比如我们访问的是32位数据类型的变量,那么索引N就代表第N个槽(第0个存储的是this,是对当前方法所属对象实例的引用)
sdasd当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。
sdd【注】:
sdsssssssdreference类型:表示对一个对象实例的引用,其实在《Java虚拟机规范》中并没有规定它的长度以及结构(它的长度与实际使用32位还是64位虚拟机有关,如果是64位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里我们暂且只取32位虚拟机的reference)。但是它要实现两个功能:①、根据引用可以直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引。二是根据引用直接或者间接的查找到对象所属数据类型在方法区中的存储的类型信息。
sdsssssssdreturnAddress类型:目前基本不用,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,通常用来实现异常的处理的跳转,但是现在已经采用 异常表 来代替了。
sdsssssssd对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。无论读写两个连续的变量槽是否为原子操作,都不会引起数据竞争和线程安全问题,因为局部变量表是线程私有的。并且只能同时访问这两个连续槽,否则在类加载的校验阶段会抛出异常。
sdasd为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以 重用 的,虽然这样能节省栈帧空间,但是也会产生一些副作用,因为不会自主gc,不过通过JIT各种优化后,就解决了这个问题。


操作数栈是什么?

sdasd操作数栈,也称为"操作栈",是一个先入后出的栈。
sdasd和局部变量表一样,操作数栈的最大深度在编译的时候就会被写入到 Code属性的max_stacks数据项 之中。
sdasd操作数栈的每一个元素都可以包含long,double在内的任何数据类型,32位数据类型所占的栈容量为1,64位的栈容量为2。
sdasd在概念模型中,两个不同的栈帧作为不同方法的虚拟机栈的元素,是相互独立的。
sdasdJava虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作数栈。
sdasd优化:让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递
ssdsddasd在这里插入图片描述


动态链接?

sdasd每个栈帧包含一个 指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
sdasdClass文件的常量池中存有大量的符号引用,字节码的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用分为两部分:
sasdasddasd①、静态解析: 一部分符号引用在类加载阶段或者第一次使用的时候被转化为直接引用。
sasdasddasd②、动态解析: 在每一次运行期间都转换为直接引用。

方法返回地址说一说?

sdasd当一个方法开始执行后,只有两种方式退出这个方法:
sdasadsadsd①、正常调用完成: 执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者。
sdasadsadsd②、异常调用完成: 是在方法执行的过程中遇到了异常,并且在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的
sdd【注】: 无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。
ssasadsadd方法正常退出时, 主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。
ssasadsadd而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。
ssasadsadd方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能(基于概念模型的分析) 执行的操作有:①、恢复上层方法的局部变量表和操作数栈,②、把返回值(如果有的话)压入调用者栈帧的操作数栈中;③、调整PC计数器的值以指向方法调用指令后面的一条指令等


方法调用的相关问题
方法调用到底指什么呢?为什么复杂?为什么具有动态扩展能力?

aas方法调用阶段的唯一任务就是确定被调用哪个方法。并没有涉及到方法内部的具体运行过程。

aas 因为一切方法的调用在Class文件里面存储的都只是符号引用(间接引用),而不是方法在实际运行时内存布局中的入口地址。某些调用需要在类加载时期甚至运行期间才能确定目标方法的直接引用。
aas[注]: Class文件的编译过程中不包含传统编译中的连接步骤。


类加载的哪个阶段可以进行 一部分 方法调用呢(间接引用—>直接引用) (解析)?

aas所有方法调用的目标方法在Class文件里面都是一个常量池的符号引用。在类加载的解析阶段,我们会将其中的一部分符号引用转化为直接引用( 另一部分需要到运行阶段才能确定)
aas[注]:在解析阶段能够进行转化的前提条件:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析。


在JVM中支持几种方法调用的字节码指令?(五个)

aas①、invokestatic: 调用静态方法:
aas②、invokespecial:用于调用实例构造器< init >()方法、私有方法和父类中的方法。
aas③、invokevirtual:调用所有的虚方法;
aas④、invokeinterface:调用接口方法,会在运行时再确定一个实现该接口的对象。
aas⑤、invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
aas[注]:
aas①、前面4条调用指令、分派逻辑都固化在JVM内部,最后一条的分派逻辑由用户设定的引导方法来决定。
aas②、只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本。


什么样的方法适合在类加载的解析阶段进行解析呢 ?

aas只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本。

aas①、静态方法 ②、私有方法 。 ③、实例构造器 ④、父类方法 aas⑤、被final修饰的方法(尽管它使用invokevirtual指令调用)
aas[注]:
adsas①、这五种方法调用都能在类加载的解析阶段把符号引用解析为该方法的直接引用。
adsas②、虚拟机层面:这五种方法统称为"非虚"方法。
adsas③、Java语言层面:Java对象里面的方法默认(即不使用final修饰)就是虚方法(还是记前者吧)。


类加载的解析阶段是动态还是静态的?方法调用处了方法解析还有什么(方法分派)?

aas解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。
aas而另一种主要的方法调用形式:分派。


分派是静态的还是动态的 ?

aas它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。


方法分派体现了什么 ?(“重载”、“重写”)

aasJava具备面向对象的三个基本特征:继承、封装、多态。而我们将要分析的方法分派将会体现出一些多态性特征(“重载”、“重写”)。

aas方法分派分为静态分派和动态分派。


重载本质之静态分派 ----说一说重载?

aas所有 依赖静态类型 来决定方法执行版本的分派动作,都称为静态分派
aas静态分派的最典型应用表现就是方法重载。因此虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据的。
aas静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

aasdasdsadasdsadasdsadsadsadasddasdsadasHuman man = new Man()

aasdasdsaddasddassadsds“Human”称为变量的“静态类型”,“Man”则被称为变量的“实际类型(运行时类型)”
aas静态类型和实际类型在程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且 最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

aasdasas①、Javac编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一”的,往往只能确定一个“相对更合适的”版本。
aasdasas②、重载方法匹配优先级:当前参数类型的类型>往大转型>封装>装箱类的接口方法>装箱后的父类>可见变长参数。


解析与分派之间的关系 ?

aas不是对立关系,而是在不同角度去筛选、确定目标方法的过程。例如前面说过静态方法会在编译器确定、在类加载阶段进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。


重写本质之动态分派 ----说一说重写? (和invokevirtual以及描述符有关)

aas在运行期根据 实际类型 确定方法执行版本的分派过程称为动态分派。 aas动态分派的最典型应用表现就是方法重写。

package org.fenixsoft.polymorphic;

	public class DynamicDispatch {	static abstract class Human {	protected abstract void sayHello();}
	static class Man extends Human {@Override
		protected void sayHello() { 	System.out.println("man say hello");}}
	static class Woman extends Human {@Override
		protected void sayHello() {		System.out.println("woman say hello");}
	}
	public static void main(String[] args) {
		Human man = new Man();
		Human woman = new Woman();
		man.sayHello();
		woman.sayHello();
		man = new Woman();
		man.sayHello();
	}
}
结果:man say hello		woman say hello			woman say hello

aas①、对于Human man = new Man();Human woman = new Woman();调用Man和Woman类的实例构造器(invokespecial),并将这两个实例的引用存放在第1、2个局部变量表的变量槽中。

aas②、在执行重写的方法时,aload指令会把刚刚创建的对象的引用压到栈顶(这里放入的是①步骤中创建的实际对象,也就是执行sayHello()方法的所有者)。

aas③、方法调用指令,这两个对象的两条调用指令单从字节码角度来看,无论是指令(都是invokevirtual)还是参数(都是常量池中同一项的常量 (Human.sayHello()的描述符和简单名称) 都完全一样,但是这两句指令最终执行的目标方法并不相同因此要弄清楚它们的不同,还是要看 invokevirtual 指令。
aa
invokevirtual指令的运行时解析过程大致分为以下几步:(这里开始回答就行)
aasa①、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
aasa②、如果在类型C中找到与常量中的 描述符和简单名称 都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
aasa③、否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
aasa④、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
aa
aasa正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中 方法重写的本质


字段有重写吗?

aasa这种多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,那自然我们得出的结论就只会对方法有效,对字段是无效的,因为字段不使用这条指令。并且只有虚方法的存在,没有虚字段的存在。即,字段永远不参与多态。当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会 遮蔽 父类的同名字段。


什么叫单分派什么叫多分派?(Java是一个静态多分派,动态单分派)

aas方法的接收者(实际类型)与方法的参数统称为方法的宗量

aas单分派: 根据一个宗量对目标进行选择; aas多分派: 根据多于一个宗量对目标进行选择;
aasdasdsadasdsadasdsadsadsadasddasdsadas单分派和多分派

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 choose 360			son choose qq

结果分析:
aasa我们关注的首先是编译阶段中编译器的选择过程,也就是静态分派的过程。这时候选择目标方法的依据有两点一是静态类型是Father还是Son,二是方法参数是QQ还是360。(这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father::hardChoice(360)及Father::hardChoice(QQ)方法的符号引用。)
aasa【注】: 因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。
aasa再看运行阶段中虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoice(new QQ())”这行代码时,更准确地说,是在执行这行代码所对应的invokevirtual指令时,由于编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的参数“QQ”到底是“腾讯QQ”还是“奇瑞QQ”,因为这时候参数的静态类型、实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有该方法的接受者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型
dsadsaddaadsa在这里插入图片描述


虚拟机动态分派到底是如何实现的呢?(对于重写的优化?方法区的优化)

aasa动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,真正运行时一般不会如此频繁地去反复搜索类型元数据。
aasa优化方式:在方法区中建立一个 虚方法表(vtable),在invokeinterface执行时也会用到接口方法表(itable)。使用虚方法表索引来代替元数据查找以提高性能。
aasa【注】:这里的“提高性能”是相对于直接搜索元数据来说的,实际上在HotSpot虚拟机的实现中,直接去查 itablevtable 已经算是最慢的一种分派,只在解释执行状态时使用,在即时编译执行时,会有更多的性能优化措施)(比如呢?)。
aasa虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址
aasa【注】:为了程序实现方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕(准备阶段)


多态概念的争议

sdas重写肯定是多态性的体现,但对于重载算不算多态,有一些概念上的争议。
sdas有观点认为必须是 多个不同类对象对同一签名的方法做出不同响应才算多态,也有观点认为只要使用同一形式的接口去实现不同类的行为就算多态


JVM什么时候开始支持动态类型语言?

sdad 目前已经有许多动态类型语言运行于Java虚拟机之上了,如Clojure、Groovy、Jython和JRuby等,也就是说我们 可以在JVM上实现静态类语言的严谨与动态类型语言的灵活
sd ad但是JDK7以前,JVM层面对动态类型语言的支持一直都还有所欠缺,主要表现在方法调用方面,只有4条方法调用指令(invokevirtual、invokespecial、invokestatic、invokeinterface),且它们的第一个参数(操作数)都是被调用的方法的符号引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量)。
sda
sdad这时就有一个问题:方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定方法的接收者。

sdad ①、曲线救国:比如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配。但是这样有两个缺点:
sdaadⅠ、让动态类型语言实现的复杂度增加,也会带来额外的性能和内存开销sdadⅡ、由于无法确定调用对象的静态类型,而导致的方法内联无法有效进行 (为什么无法确定呢,是因为编译留了个占位符导致的吗?)
sda
sdaad2、基于以上背景,在JVM层面上提供动态类型的直接支持就成为Java平台发展必须解决的问题,直到JDK 7,JSR-292提出了invokedynamic指令以及java.lang.invoke包解决了此问题。


invoke包你了解吗

sdad这个包的主要目的:在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为“方法句柄”(Method Handle)
sda
sdad[注]: 在Java语言中做不到像C那样,单独把一个函数作为参数(函数指针的功劳)进行传递。普遍的做法是设计一个带有compare()方法的Comparator接口,以实现这个接口的对象作为参数。比如void sort(List list, Comparator c)
sda
sdad不过在有了方法句柄之后,Java语言就可以像C那样,拥有类似于函数指针或者委托的方法别名这样的工具了。

sdad方法getPrintlnMH()中实际上是模拟了invokevirtual指令的执行过程,只不过它的分派逻辑并非固化在Class文件的字节码上,而是通过一个由用户设计的Java方法来实现。而这个方法本身的返回值(MethodHandle对象),可以视为对最终调用方法的一个“引用”。


方法句柄和反射的关系?

区别:
sasad①、ReflectionMethodHandle机制本质上都是在模拟方法调用。但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的3个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual(以及invokeinterface)和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的
sasad②、Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。
ssadasdasad前者是方法在Java端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含执行权限等的运行期信息。
ssadasdasad后者仅包含执行该方法的相关信息
sasad③、Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言。
sasad总之,Reflection是重量级,而MethodHandle是轻量级。
sasad[注]:由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),MethodHandle上也应当可以采用类似思路去支持(但目前实现还在继续完善中),而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。


invokedynamic指令和MethodHandle方法的对比?

sdaa相同点:某种意义上可以说invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有4条“invoke*”指令方法分派规则完全固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(广义的用户,包含其他程序语言的设计者)有更高的自由度。
sdasdasdasda
sdaa不同点invokedynamic指令用字节码和Class中其他属性、常量来完成。MethodHandle是用上层代码和API来实现。

sdaa[注]: 每一处含有invokedynamic指令的位置都被称作"动态调用点"。这条指令的第一个参数(操作数)不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7时新加入的CONSTANT_InvokeDynamic_info常量,

sdaa[注]: 由于invokedynamic指令面向的主要服务对象并非Java语言,而是其他Java虚拟机之上的其他动态类型语言,因此,光靠Java语言的编译器Javac的话,在JDK 7时甚至还完全没有办法生成带有invokedynamic指令的字节码(曾经有一个java.dyn.InvokeDynamic的语法糖可以实现,但后来被取消了),而到JDK 8引入了Lambda表达式和接口默认方法后,Java语言才算享受到了一点invokedynamic指令的好处,但用Lambda来解释invokedynamic指令运作就比较别扭,也无法与前面MethodHandle的例子对应类比,所以笔者采用一些变通的办法:John Rose编写过一个把程序的字节码转换为使用invokedynamic的简单工具INDY来完成这件事。


Java内存模型与线程
多任务处理为什么在OS中几乎是一项必备的功能

sadsa①、计算机的运算能力强大了,但其运算速度与它的存储和 通信子系统的速度 差距太大了,不匹配,大量的时间都花费在 磁盘I/O、网络通信或者数据库访问 上。为了避免处理器大部分时间处于"空闲状态",所以我们要让其动起来,避免造成很大的性能浪费。
sadsa②、一个 服务器要对多个客户端提供服务,这是Java语言最擅长的领域之一,我们通过参数:TPS(每秒事务处理数)来衡量服务性能的好坏。


为什么内存和处理器之间要加入"高速缓存"?

sadsa因为计算机的存储设备与处理器的运算速度有着几个数量级的差距,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲。
sadsa[注]:基于高速缓存的存储交互很好地解决了处理器与内存速度之间的矛盾,但是也为计算机系统带来更高的复杂度,它引入了一个新的问题:缓存一致性(Cache Coherence)


什么是“缓存一致性”问题呢?

sadsa在多路处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,这就是"缓存一致性"问题。如图:
在这里插入图片描述


什么是乱序执行呢?

sadsa为了使 处理器内部的运算单元 能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。
sadsa[注]:Java虚拟机的即时编译器中也有指令重排序优化。


什么是Java内存模型?定义Java内存模型要注意什么?

sadsa用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

sadsa①、这个模型必须定义得足够 严谨,才能让Java的并发内存访问操作不会产生歧义;
sadsa②、必须定义得足够 宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存和指令集中某些特有的指令)来获取更好的执行速度。


Java内存模型的目的是什么?

sadsaJava内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。
sadsa[注]:
sadsa①、此处的变量包括实例变量、静态变量和构建数组对象的元素,但是 不包括局部变量与方法参数。因为后者是线程私有的,不会共享。
sadsa②、为了获得更好的执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器是否要进行调整代码执行顺序这类优化措施
sads
ssadsadsadadasdsadsasdasdasdsasadsadasdadsaJava内存模型
在这里插入图片描述
sadsa[注]:这里的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的对内存的划分,这两者基本上是没有任何关系的。
sadsa从功能角度可以这样划分:
sadsadasda①、主内存主要对应于Java堆中的对象实例数据部分。
sadsadasda②、工作内存则对应于虚拟机栈中的部分区域。
sadsa从更基础的层次上:
sadsadasda①、主内存直接对应于物理硬件的内存 。
sadsadasda②、而为了获取更好的运行速度,虚拟机(或者是硬件、操作系统本身的优化措施)可能会让工作内存优先存储于 寄存器和高速缓存 中。

内存间的交互操作有几种?

sadsa关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了8种操作(lock、unlock、read、load、assign、use、store、write)来完成。但是,为了使用方便,进行了简化,如今只用了4种:
sadsacaddsasadsadssadasadsa①、read ②、write ③、lock ④、unlock
在这里插入图片描述
sadsa[注]:这只是语言描述上的等价化简,Java内存模型的基础设计并未改变(仍需要8种操作) 。


线程的实现

sa实现线程主要有3种方式:

sadasdasdsadsa①、内核线程实现(1:1) ②、用户线程实现(1:N)③、用户线程加轻量级进程混合实现(N:M)。


内核线程实现 (Windows)(1 : 1 实现) ?

sadsa内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核
sadsa
sadsa程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程
dsadsadsadassadasd在这里插入图片描述
sadsa优点: 由于内核线程(KLT)的支持,每个轻量级进程(LWT)都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作。
sadsa缺点:
saddassa①、由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和内核态中来回切换。
sasdadsa②、每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。


用户线程实现 (1 : N 实现) ? (不常用)

sadsa广义上来讲:一个线程只要不是内核线程,都可以认为是用户线程(User Thread,UT)的一种,因此从这个定义上看,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,因此效率会受到限制,并不具备通常意义上的用户线程的优点
sadsa
sadsa狭义上来讲:用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。
sadsa
sadsa进程与用户线程之间1:N的关系称为一对多的线程模型。如图:
ssdsddsdsdads在这里插入图片描述

sadsa优点: 用户线程不同于轻量级进程,前者不需要内核线程支援。
sadsa缺点:
saddassa①、 由于没有内核线程支持,所有的线程操作都需要由用户程序自己去处理。
saddassa②、而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的。


混合实现:(Unix) (N :M)

sadsa线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式,被称为N:M实现。
sadsa在这种混合实现下,既存在 用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而 操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险
dasdsdsdsddasd在这里插入图片描述


HotSpot虚拟机支持哪种线程模式?(1:1)

sadsa它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以HotSpot自己是不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议),全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的。(Windows)


Java线程调度问题?

sadsdsdsdsdsdsddsa调度主要方式有两种:①、协同式线程调度 ②、抢占式线程调度
协同式线程调度:
sadssa线程的执行时间由线程本身来控制。线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去

sadssa好处: 实现简单,由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以一般没有什么线程同步的问题.
sadssa缺点: 线程执行时间不可控制,甚至如果一个线程的代码编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。

抢占式调度 (Java使用的线程调度方式就是抢占式调度.):
sadssa线程的切换不由线程本身来决定,且线程的执行时间是系统可控的,也不会有一个线程导致整个进程甚至整个系统阻塞的问题。
Java线程调度是系统自动完成,那我们是否还能"干预"其执行的时间长短呢?
sadssa可以,我们可以通过设置线程优先级来“建议”操作系统给某些线程多分配一点执行时间,另外的一些线程则可以少分配一点。
sadssa【注】:Java语言一共设置了10个级别的线程优先级,默认为5.


线程优先级是一种稳定的调节手段吗?(不是)(两方面)

sadssa①、因为主流虚拟机上的Java线程是被映射到系统的原生线程上(KLT)来实现的,所以线程调度最终还是由操作系统说了算 尽管现代的操作系统基本都提供线程优先级的概念,但是并不见得能与Java线程的优先级一一对应。如果操作系统的优先级比Java线程优先级更多,那问题还比较好处理,中间留出一点空位就是了,但对于比Java线程优先级少的系统,就不得不出现几个线程优先级对应到同一个操作系统优先级的情况了。
sadssa【注】:Windows线程优先级就比Java语言的优先级个数少,所以存在多对一的情况。

sadssa②、优先级可能会被系统自行改变,例如在Windows系统中存在一个叫“优先级推进器”的功能(当然它可以被关掉),大致作用是 当系统发现一个线程被执行得特别频繁时,可能会越过线程优先级去为它分配执行时间,从而减少因为线程频繁切换而带来的性能损耗。因此,我们并不能在程序中通过优先级来完全准确判断一组状态都为Ready的线程将会先执行哪一个


内核线程对当今互联网有啥局限性?

sadssa如今对Web应用的服务要求,不论是在请求数量上还是在复杂度上都非与过去有了很大的提升。现代B/S系统中一次对外部业务请求的响应,往往需要分布在不同机器上的大量服务共同协作来实现,这种服务细分的架构在减少单个服务复杂度、增加复用性的同时,也不可避免地增加了服务的数量,缩短了留给每个服务的响应时间。这样对服务器的要求:
sadsssda① 、每一个服务都必须在极短的时间内完成计算,这样组合多个服务的总耗时才不会太长
saddasda② 、要求每一个服务提供者都要能同时处理数量更庞大的请求,这样才不会出现请求由于某个服务被阻塞而出现等待
sadssa而Java目前的并发编程机制就与刚才说的架构趋势产生了一些矛盾,1:1的内核线程模型是如今Java虚拟机线程实现的主流选择,但是这种映射到操作系统上的线程 天然的缺陷是切换、调度成本高昂,系统能容纳的线程数量也很有限。 放在过去或许无伤大雅,但现在的每个请求的时间变得很短,而数量很多得前提下,用户线程切换的开销甚至可能会接近用于计算本身的开销,这就会造成严重的浪费。
sadssa【为什么内核线程调度切换起来成本就要更高?】:内核线程的调度成本主要来自于用户态与核心态之间的状态转换,而这两种状态转换的开销主要来自于响应中断、保护和恢复执行现场的成本。


内核线程调度切换成本高,那如果换成用户线程呢?用户线程的优化?栈纠缠?

sadssa同样不能,但是我们可以把保护和恢复执行现场及调度的工作从操作系统交到程序员手上。而靠人的智慧,我们可以通过一些方法来缩减这些开销:栈纠缠(Stack Twine)

栈纠缠是什么意思?
sadssa由用户自己模拟多线程、自己保护恢复现场的工作模式。其大致的原理是通过在内存里划出一片额外空间来模拟调用栈,只要其他“线程”中方法压栈、退栈时遵守规则,不破坏这片空间即可,这样多段代码执行时就会像相互缠绕着一样,非常形象


协程的由来?优缺点?纤程?系统调度?

sadssa随着操作系统开始提供多线程的支持,靠应用自己模拟多线程的做法(栈纠缠)就慢慢演化为了用户线程,而那时候多数用户线程用的调度方式是协同式调度,所以我们称其为"协程"。

协程的优缺点?
sadssa协程的主要优势: 轻量,无论是有栈协程还是无栈协程,都要比传统内核线程要轻量得多。

sadssa【注】:由于这时候的协程会完整地做调用栈的保护、恢复工作,所以今天也被称为“有栈协程”,起这样的名字是为了便于跟后来的“无栈协程”区分开。

sadssa协程的局限性

sadssssa①、需要在应用层面实现的内容(调用栈、调度器这些) 特别多。sadssssa②、顾名思义,"协同调度"的缺点。
sadssa
有栈协程的替代:ssa对于有栈协程,有一种特例实现名为纤程(Fiber)。最早微软提出来的。

系统调度 (
在这里插入图片描述


线程安全问题?
什么叫线程安全?

aas 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

线程安全的“安全程度”有什么等级?

aaasdass①、不可变 ②、绝对线程安全 ③、相对线程安全 ④、线程兼容 ⑤、线程对立


什么叫不可变 ?

aaasdass不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。
aaasdass【注】:
aaasdasadssfinal : ①、“final关键字带来的可见性”时曾经提到过这一点:只要一个不可变的对象被正确地构建出来(即没有发生this引用逃逸的情况),那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态
aaasdasadss②、基本数据类型的包装类:Java语言中,如果多线程共享的数据是一个 基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的(对于对象,还没有相应的支持方式,API中符合不可变要求的类型有String之外,常用的还有枚举类型及java.lang.Number的部分子类,如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。但同为Number子类型的原子类AtomicInteger和AtomicLong则是可变的)。
aaasdasadss③、“不可变”带来的安全性是最直接、最纯粹的。


什么叫绝对线程安全 ?(一般没有)

aaasdass绝对的线程安全能够完全满足我们文章开头定义的线程安全概念(也就是说更加严格 )。(调用者都不需要任何额外的同步措施”可能需要付出非常高昂的,甚至不切实际的代价)
aaasdass【注】:
aaasdasadss①、在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
aaasdasadss②、比如java.util.Vector是一个线程安全的容器(因为它的add()、get()和size()等方法都是被synchronized修饰的,尽管这样效率不高,但保证了具备原子性、可见性和有序性。),但是在多线程环境中,如果不在方法调用端做额外的同步措施,使用这段代码仍然是不安全(不是绝对线程安全)的。(假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次对其中元素进行改动都要产生新的快照,这样要付出的时间和空间成本都是非常大的。)


什么叫相对线程安全 ?(我们意义中的线程安全)

aaasdass相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
aaasdass【注】:在Java语言中,大部分声称线程安全的类都属于这种类型,例如:Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。


什么叫线程兼容 ?(Java类库API中大部分的类都是线程兼容的。)

aaasdass线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
aaasdass【注】:我们平常说一个类不是线程安全的,通常就是指这种情况。


什么叫线程对立 ?

aaasdass线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。


线程安全的实现方法 之 互斥同步 ?

aaasdass互斥同步是一种最常见也是最主要的并发正确性保障手段,临界区、互斥量和信号量 都是常见的互斥实实现方式


线程安全的实现方法 之 非阻塞同步 ?

aaasdass互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步。
aaasdass互斥同步属于一种悲观的并发策略,而随着硬件指令集的发展,我们已经有了另外一个选择:基于冲突检测的乐观并发策略。通俗地说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步,使用这种措施的代码也常被称为无锁编程。

aaasdass【注】:
aaasdass①、为什么要说随着硬件指令集的发展? 因为我们必须 要求操作和冲突检测 这两个步骤具备原子性。

aaasdass②、靠什么来保证原子性?这里再使用互斥同步来保证就完全失去意义了,所以我们只能靠硬件来实现这件事情,硬件保证某些从语义上看起来需要多次操作的行为可以 只通过一条处理器指令 就能完成。
aaasdass常用的指令:
aaassaddass⒈、测试并设置(Test-and-Set)
aaassaddass⒉、获取并增加(Fetch-and-Increment)
aaassaddass⒊、交换(Swap)
aaassaddass⒋、比较并交换(CAS);
aaassaddass⒌、加载链接/条件储存(Load-Linked/Store-Conditional,下文称LL/SC)。
aaasdass后面的两条是现代处理器新增的,功能也类似。(在IA64、x86指令集中有用 cmpxchg 指令完成的CAS功能。)


线程安全的实现方法 之 无同步方案 ?

aaasdass要保证线程安全,也并非一定要进行阻塞或非阻塞同步,同步与线程安全两者没有必然的联系。
aaa
aaasdass同步只是保障存在共享数据争用时正确性的手段,如果能让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的。两类:

aaasdsaasda①、可重入代码: 又称纯代码,指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。
aaasdass【如何判断代码具备可重入性?】如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。

aaasdsaasda②、线程本地存储 (ThreadLocal): 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。


线程优化问题
什么叫自旋锁?优化(自适应自旋锁)?(JDK 6中就已经改为默认开启,-XX:+UseSpinning)

aas互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入 内核态 中完成。但在许多应用上,共享数据的锁定状态 只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。因此,我们可以让没有请求到锁的线程等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快释放锁。而这个等待的操作,我们让线程执行忙循环(自旋)来实现,这就是所谓的自旋锁

aas自旋锁的性能分析:自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间:如果锁被占用的时间很短,自旋等待的效果就会非常好;如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的浪费。(户也可以使用参数-XX:PreBlockSpin 来自行更改。)

自适应自旋锁(自旋锁的优化):
aas而在JDK 6中对自旋锁的优化,引入了自适应的自旋。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
aaasds如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环。
aaasds另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。


什么叫锁粗化 ?

aas如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。因为这时我们需要适当扩大同步块的作用范围。比如:在for循环中的同步块放到for循环外。


什么叫锁消除 ?

aas锁消除是指虚拟机 即时编译器(JIT) 在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。(锁消除的主要判定依据来源于 逃逸分析的数据支持))
aas如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。

aas[注]:比如对字符串的连接操作,Javac编译器会对String连接做自动优化,在JDK 5之前,字符串加法会转化为StringBuffer对象的连续append()操作,在JDK 5及以后的版本中,会转化为StringBuilder对象的连续append()操作。而每个StringBuffer.append()方法中都有一个同步块,而虚拟机观经过逃逸分析后会发现同步块锁的对象的动态作用域被限制在concatString() 方法内部。也就是该对象的所有引用都永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地消除掉(在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行)。


什么叫轻量级锁 ?轻量级锁的工作过程?为什么轻量(和CAS有关)呢?

aas轻量级锁是JDK 6时加入的新型锁机制,它名字中的“轻量级”是 相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。

aas设计的初衷:不是用来替代重量级锁,而是在没有锁竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
aasHotSpot虚拟机的对象头分为两部分:

aas第一部分:用于存储对象自身的运行时数据,如哈希码、GC分代年龄等。这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,称为 “Mark Word”,这部分是实现 轻量级锁和偏向锁 的关键。

aas另外一部分:用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。

aas[注]:由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率, Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。
sadasddsaddsadasdsasd在这里插入图片描述
aas轻量级锁的工作过程:

aas在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方为这份拷贝加了一个Displaced前缀,即 Displaced Mark Word)。
asdsdsdsdsdsdsddsdas在这里插入图片描述
aas然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。
asdsdsdsdsdsdsddsdas在这里插入图片描述
aas上面描述的是轻量级锁的加锁过程,它的解锁过程也同样是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程

aas[注]:轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢


什么叫偏向锁 ?偏向锁和哈希码的关系?重量级锁和哈希码的关系?

aas偏向锁也是JDK 6中引入的一项锁优化措施,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。

aas如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。

aas“偏”:锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

aas启用参数:-XX:+UseBiased Locking,JDK 6起HotSpot虚拟机的默认开启)

aas当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时 使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。

aas一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行。

aas当对象进入偏向状态的时候,Mark Word大部分的空间(23个比特)都用于存储持有锁的线程ID了,这部分空间占用了原有存储对象哈希码的位置,那原来对象的哈希码怎么办呢?

aas因此,当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。(在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码。)

aas[注]:偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。


前端编译与优化问题
"编译期"到底指什么?

在这里插入图片描述
aaasdas①、一般我们说的“前端”指的是由前端编译器完成的编译行为。
aaasdas②、Java虚拟机设计团队选择把对性能的优化全部集中到运行期的即时编译器中(这样可以让那些不是由Javac产生的Class文件(如JRuby、Groovy等语言的Class文件)也同样能享受到编译器优化措施所带来的性能红利)。
aaasdas③、相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖字节码或者Java虚拟机的底层改进来支持。
aaasdas总之:Java中即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升;而前端编译器在编译期的优化过程,则是支撑着程序员的编码效率和语言使用者的幸福感的提高。


Javac编译器 (解释器)的编译过程 (Javac编译器身就是一个由Java语言编写的程序)?符号表的作用?插入式注解器的作用?常量折叠?局部变量是否用final修饰对于Class文件都是一样?idea“点击”后执行流程?语法糖(泛型、变长参数、自动封箱装箱)?中间代码的生成?注解?

dasJavac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程:

dasdas①、准备过程:初始化插入式注解处理器。

dasdas②、解析与填充符号表过程(词法、语法分析;构造出抽象语法树;填充符号表:产生符号地址和符号信息)

dasdas③、插入式注解处理器的 注解处理过程

dasdas④、分析与字节码生成过程(标注检查(对语法的静态信息进行检查)、数据流及控制流分析(对程序动态运行过程进行检查)、解语法糖(将简化代码编写的语法糖还原为原有的形式)、字节码生成(将前面各个步骤所生成的信息转化成字节码))

dasdas上述3个处理过程里,执行插入式注解时 又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号
dasddsdsddsdsdas在这里插入图片描述
aas1.1 解析过程: 包括了词法分析和语法分析两个步骤:
aa
aaasdsas①、词法分析是将 源代码的字符流 转变为 标记集合 的过程。(字符流 ⏩ 标记集合)

aaasdasdsasas单个字符是程序编写时的最小元素,但标记才是编译时的最小元素,键字、变量名、字面量、运算符都可以作为标记。比如,“int a=b+2”这句代码中就包含了6个标记,分别是int、a、=、b、+、2。
aa
aaasdsas②、语法分析是 根据标记序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至连代码注释等都可以是一种特定的语法结构。 ( 构建抽象语法树)
aa
aas1.2 填充符号表过程: 完成了语法分析和词法分析之后,对符号表进行填充的过程。
aa
aaasdsas①、符号表是由一组符号地址和符号信息构成的数据结构,读者可以把它类比想象成哈希表中键值对的存储形式(实际上符号表不一定是哈希表实现,可以是有序符号表、树状符号表、栈结构符号表等各种形式)

aaasdsas②、符号表中所登记的信息在编译的不同阶段都要被用到。譬如在语义分析的过程中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的声明是否一致)和产生中间代码,在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的直接依据。(对于语义分析的验证和生成中间代码并且符号表是地址分配的直接依据)

aas2 注解处理器:
aaasdsas①、JDK 5之后,Java语言提供了对注解的支持,注解在设计上原本是与普通的Java代码一样,都只会在程序运行期间发挥作用的。但在JDK 6中又设计了一组被称为“插入式注解处理器”的标准API,可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程。
aasadsas[注]:
aasasdsaasas⒈我们可以把插入式注解处理器看作是一组编译器的 插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理。
aasdsasadsas⒉有了编译器注解处理的标准API后,程序员的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件中被访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。比如Java著名的编码效率工具Lombok,它可以通过注解来实现自动产生getter/setter方法、进行空置检查、生成受查异常表、产生equals()和hashCode()方法,等等,帮助开发人员消除Java的冗长代码,这些都是依赖插入式注解处理器来实现的。

aas3 语义分析与字节码生成: (依靠抽象语法树对程序逻辑进行验证)

aaasdsas语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查,等等。

aaasdsasJavac在编译过程中,语义分析过程可分为标注检查数据及控制流分析两个步骤(当然还有其他),
aa
aaasdsas⒈标注检查:
aaasasddsas检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配。在标注检查中,还会顺便进行一个称为 常量折叠 的代码优化,这是Javac编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
aasasdadsas[注]:比如对于int a = 1 + 2,在抽象语法树上仍然能看到字面量“1”“2”和操作符“+”号,但在经过常量折叠优化之后,它们将被折叠为"3"。因此,在代码里面定义“a=1+2”比起直接定义“a=3”来,并不会增加程序运行期哪怕仅仅一个处理器时钟周期的处理工作量。
aaasdsas⒉数据及控制流分析
aaasasddsas数据流分析和控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序 局部变量 在使用前是否有赋值、方法的每条路径是否都有 返回值、是否所有的 受查异常 都被正确处理了等问题;
aasasdadsas[注]:
aasasdadsasds①、编译时期的数据及控制流分析与类加载时的数据及控制流分析的目的基本上可以看作是一致的,但校验范围会有所区别,有一些校验项只有在编译期或运行期才能进行。
aasasdadsasds②、【局部变量是否用final修饰对于Class文件都是一样?】 局部变量在常量池中并没有CONSTANT_Fieldref_info的符号引用,自然就不可能存储有访问标志(access_flags)的信息,甚至可能连变量名称都不一定会被保留下来(这取决于编译时的编译器的参数选项),自然在Class文件中就不可能知道一个局部变量是不是被声明为final了。因此,可以肯定地推断出把局部变量声明为final,对运行期是完全没有影响的,变量的不变性仅仅由Javac编译器在编译期间来保障。
aaasdsas⒊解释语法糖
aaasasddsas语法糖,也称糖衣语法,由英国计算机科学家Peter J.Landin发明的一种编程术语,指的是在计算机语言中添加的某种语法。这种语法对语言的编译结果和功能并没有实际影响,但是却能更方便程序员使用该语言通常来说使用语法糖能够减少代码量、增加程序的可读性,从而减少程序代码出错的机会。
aasasdadsas[注]:
aasasdadsasds①、Java在现代编程语言之中已经属于“低糖语言”(这也是Java语言一直被质疑是否已经“落后”了的一个浮于表面的理由)。
aasasdadsasds②、Java中最常见的语法糖包括了前面提到过的 泛型、变长参数、自动装箱拆箱,等等。Java虚拟机运行时并不直接支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程就称为解语法糖。

aaasdsas⒋字节码生成

aaasasddsas字节码生成是Javac编译过程的最后一个阶段。

aaasasddsas字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,编译器还进行了少量的代码添加和转换工作。(在这个时期把实例构造器< init>()方法和类构造器< clinit>()方法就是在这个阶段被添加到语法树之中的)
aasasdadsas[注]:
aasasdadsasds①、这里的实例构造器并不等同于默认构造函数,如果用户代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、可访问性(public、protected、private或< package>)与当前类型一致的默认构造函数,这个工作在 填充符号表阶段中就已经完成
aasasdadsasds②、< init>()和< clinit>()这两个构造器的产生实际上是一种代码收敛的过程,编译器会把语句块、调用父类的实例构造器(仅仅是实例构造器,< clinit>()方法中无须调用父类的()方法,Java虚拟机会自动保证父类构造器的正确执行,但在< clinit>()方法中经常会生成调用java.lang.Object的< init>()方法的代码)等操作收敛到< init>()和< clinit>()方法之中,并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行,上面所述的动作由Gen::normalizeDefs()方法来实现。除了生成构造器以外,还有其他的一些代码替换工作用于优化程序某些逻辑的实现方式,如把字符串的加操作替换为StringBuffer或StringBuilder(取决于目标代码的版本是否大于或等于JDK 5)的append()操作,等等。
aaasasddsas③、完成了对语法树的遍历和调整之后,就会把填充了所有所需信息的符号表到com.sun.tools.javac.jvm.ClassWriter类手上,由这个类的writeClass()方法输出字节码,生成最终的Class文件,到此,整个编译过程宣告结束。


Java语法糖

aas 几乎所有的编程语言都或多或少提供过一些语法糖来方便程序员的代码开发,这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会。现在也有一种观点认为语法糖并不一定都是有益的,大量添加和使用含糖的语法,容易让程序员产生依赖,无法看清语法糖的糖衣背后,程序代码的真实面目。

谈一谈泛型?(还要看)

aas泛型的本质是参数化类型或者参数化多态的应用,即可以将操作的数据类型指定为方法签名中的一种特殊参数,这种参数类型能够用在类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法。泛型让程序员能够针对泛化的数据类型编写相同的算法,这极大地增强了编程语言的类型系统及抽象能力。
aas[注]:Java和C#两门语言各自添加了泛型的语法特性,但实现方式却截然不同,其实Java的泛型直到今天依然作为Java语言不如C#语言好用。

aasC#实现的泛型
aaasdass实现方式:“具现化式泛型”,C#里面泛型无论在程序源码里面、编译后的中间语言表示(这时候泛型是一个占位符)里面,抑或是运行期的CLR (common language runtime) 里面都是切实存在的。平行地加一套泛型化版本的新类型。
aaasads[注]:List< int>与List< string>就是两个不同的类型,它们由系统在运行期生成,有着自己独立的虚方法表和类型数据。
aasJava实现的泛型
aaasdass实现方式:类型擦除式泛型,它只在程序源码中存在,在编译后的字节码文件中,全部泛型都被替换为原来的裸类型 (裸类型为所有该类型泛型化实例的共同父类型。只在元素访问、修改时自动插入一些强制类型转换和检查指令。
aaasdass并且为了保证Java 5.0之前编译出来的Class文件可以在Java 5.0引入泛型之后继续运行,java实现的泛型方式就是将已有的类型直接泛型化譬如ArrayList,原地泛型化后变成了ArrayList< T>,为了保证以前直接用ArrayList的代码在泛型版本里必须还能继续用这同一个容器,这就必须让所有泛型化的实例类型,譬如ArrayList< Integer>、ArrayList< String>这些全部自动成为ArrayList的子类型才能可以,否则类型转换就是不安全的

aaasdsads[注]:对于Java语言来说,ArrayList< int>与ArrayList< String>其实是同一个类型。

aas优点:Java中擦除式泛型的实现几乎只需要在Javac编译器上做出改进即可,不需要改动字节码、不需要改动Java虚拟机,也保证了以前没有使用泛型的库可以直接运行在Java 5.0之上。

aas缺点:
aasdas1、Java中不支持对泛型进行实例判断、不支持使用泛型创建对象、不支持使用泛型创建数组,并且对于基本数据类型需要装箱拆箱( 因为不支持int、long与Object之间的强制转型。) (装箱、拆箱的开销是Java泛型慢的重要原因)。使用擦除法实现泛型直接导致了 对原始类型数据的支持 又成了新的麻烦。(比如我们去写一个泛型版本的从List到数组的转换方法,由于不能从List中取得参数化类型T,所以不得不从一个额外参数中再传入一个数组的组件类型进去。)
aasdsasdsadsadsadssadsaadasdsadadasdas不得不加入的类型参数

public static <T> T[] convert(List<T> list, Class<T> componentType) {
	T[] array = (T[])Array.newInstance(componentType, list.size());
	...
}

aasdas2、通过擦除法来实现泛型,还丧失了一些面向对象思想应有的优雅,带来了一些模棱两可的模糊状况。比如泛型应用到重载中,方法的参数由不同的泛型实例实现,比如List< Integer>和List< String>,类型擦除导致这两个方法的特征签名变得一模一样,因此两个方法无法被编译。

aass(类型擦除式泛型)的解决措施:

aaaasdsasas①、由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响并带来新的需求,如在泛型类中如何获取传入的参数化类型等。所以《Java虚拟机规范》做出了相应的修改,引入了诸如SignatureLocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题

aaaasdssdas⒈Signature:存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。

aaaasdsasas②、从Signature属性的出现我们还可以得出结论:擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们在编码时能通过反射手段取得参数化类型的根本依据。


谈一谈自动封装?

aaas就纯技术的角度而论,自动装箱、自动拆箱与遍历循环 这些语法糖,无论是实现复杂度上还是其中蕴含的思想上都不能与泛型相提并论,两者涉及的难度和深度都有很大差距。
aa
aaas我们看一段代码,其中包含了泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖:
aasdsadasd
aasdsasdsadsadsadssadsaadasdsadadasdas编译前的代码

public static void main(String[] args) {
	List<Integer> list = Arrays.asList(1, 2, 3, 4);
	int sum = 0;
	for (int i : list) {
		sum += i;
	}
	System.out.println(sum);
}

aasdsasdsadsadsadssadsaadasdsadadasdas编译后的代码

public static void main(String[] args) {
	List list = Arrays.asList( new Integer[] {   //泛型消除
		Integer.valueOf(1),
		Integer.valueOf(2),
		Integer.valueOf(3),
		Integer.valueOf(4) });
	int sum = 0;
	for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
		int i = ((Integer)localIterator.next()).intValue();
		sum += i;
	}
	System.out.println(sum);
}

aasas分析: ⒈泛型:编译后泛型消除,List< Integer>变成了List 。

aassdasdaas⒉自动装箱、拆箱:编译之后被转化成了对应的包装和还原方法,如本例中的Integer.valueOf()intValue()方法,
aasadasdaas⒊遍历循环:编译之后代码还原成了 迭代器 的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。
aasasasdaas⒋变长参数:它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,程序员的确也就是使用数组来完成类似功能的
aasdsasdsadsadsadssadsaadasdsadadasdas自动封装带来的陷阱:

public static void main(String[] args) {
	Integer a = 1;	Integer b = 2;	Integer c = 3;
	Integer d = 3;	Integer e = 321;	Integer f = 321;
	Long g = 3L;
	System.out.print(c == d );   true
	System.out.print(e == f );   false			//超出了范围[-128,127],所以封装的地址不一样了
	System.out.print(c == (a + b) );  true   //自动拆箱了,
	System.out.print(c.equals(a + b) );   true 
	System.out.print(g == (a + b) );   true 	//“==”运算遇到了"+",自动拆箱
	System.out.print(g.equals(a + b) );  false  //equals()方法不处理数据转型的关系
}
执行结果:
true false true true true false

aasasaas分析:包装类的 “==”运算 在不遇到算术运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系,在实际编码中尽量避免这样使用自动装箱与拆箱。


谈一谈条件编译 ?

aaasJava语言实现条件编译,方法就是使用条件为常量的if语句,该代码中的if语句不同于其他Java代码,它在编译阶段就会被“运行”
aasdsasdsadsadsadssadsaadasdsadadasdasJava语言的条件编译:

public static void main(String[] args) {
	if (true) {System.out.println("block 1");} 
	else {System.out.println("block 2");}
}

aasdsasdsadsadsadssadsasadadasdas该代码编译后Class文件的反编译结果:

public static void main(String[] args) {
	System.out.println("block 1");
}

aaasJava语言中条件编译的实现,也是Java语言的一颗语法糖,根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉,这一工作将 在编译器解除语法糖阶段(com.sun.tools.javac.comp.Lower类中)完成。
aaas由于这种条件编译的实现方式使用了if语句,所以它必须遵循最基本的Java语法,只能写在方法体内部,因此它只能实现语句基本块(Block)级别的条件编译,而没有办法实现根据条件调整整个Java类的结构。
aaas总结:除了文章种说到的泛型、自动装箱、自动拆箱、遍历循环、变长参数和条件编译之外,Java语言还有不少其他的语法糖,如内部类、枚举类、断言语句、数值字面量、对枚举和字符串的switch支持、try语句中定义和关闭资源(这3个从JDK 7开始支持)、Lambda表达式(从JDK 8开始支持,Lambda不能算是单纯的语法糖,但在前端编译器中做了大量的转换工作),等等。


后端编译与优化

aas 如果我们把字节码看作是程序语言的一种中间表示形式的话,那编译器无论在何时、在何种状态下把Class文件转换成与本地基础设施(硬件指令集、操作系统)相关的二进制机器码,它都可以视为整个编译过程的后端。 无论是提前编译器抑或即时编译器,都不是Java虚拟机必需的组成部分。


如何评判虚拟机性能的好坏?

sadasd后端编译器编译性能的好坏、代码优化质量的高低 是衡量一款商用虚拟机优秀与否的关键指标之一。


什么是即时编译器?为什么要用即时编译器?

sadasd目前主流的两款商用Java虚拟机(HotSpot、OpenJ9)里,Java程序最初都是通过解释器进行解释执行的,当虚拟机遇到某个方法或者代码块的运行特别频繁,我们就把其认定为"热点代码"。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。


为何HotSpot虚拟机要使用解释器与即时编译器并存的架构?

sadasd不是所有的JVM都要采用解释器与编译器,但是目前主流的商用。JVM内部都包含解释器与编译器。当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。当程序启动后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率

sadasd当程序运行环境中内存资源紧张,可以使用解释执行(解释器)节约内存。而编译执行(编译器)来可以用来提升效率。


解释器相对于JIT的作用?(激进优化)

sadsaasd①、解释器还可以作为编译器 激进优化 时后备的“ 逃生门 ”(如果情况允许,HotSpot虚拟机中也会采用不进行激进优化的客户端编译器充当“逃生门”的角色)。
sadsaasd②、什么叫 激进优化 呢?什么叫 逆优化?让编译器根据概率选择一些不能保证所有情况都正确,但大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类以后,类型继承结构出现变化、出现“罕见陷阱”时可以通过逆优化退回到解释状态继续执行。(怎么逆优化呢?通过解释器保存信息?)
sadasd解释器与编译器经常是相辅相成地配合工作,交换关系如下图:
sadsadsaadsaadsaaas在这里插入图片描述
sadasdHotSpot虚拟机中内置了两个(或三个)即时编译器,如图:
sadsadsdsaaddsaadaadsaadsaaas在这里插入图片描述


解释器与编译器是如何工作的呢?

sadasd在分层编译的工作模式出现以前,HotSpot虚拟机通常是采用解释器与其中一个编译器直接搭配的方式工作,程序使用哪个编译器,只取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式。
sadasd[注]:
sadsaasd①、“client”:指定虚拟机运行在客户端模式;“-server”:指定虚拟机运行在服务端模式。

sadsaasd②、混合模式:解释器与编译器搭配使用;

sadsaasd③、“-Xint”:强制虚拟机运行于“解释模式”,这时候编译器完全不介入工作,全部代码都使用解释方式执行;

sadsaasd④、“-Xcomp”:强制虚拟机运行于“编译模式”,这时候将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。

sadsaasd⑤、“-version”命令的输出结果显示出这三种模式。


即时编译器有什么缺陷,怎么改善的呢?

sadasd缺点: ①、即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化程度越高的代码,所花费的时间便会越长;
sadasdasdasd②、想要编译出优化程度更高的代码,解释器可能还要 替编译器收集性能监控信息,这对解释执行阶段的速度也有所影响;
sadasd为了在程序 启动响应速度与运行效率 之间达到最佳平衡,HotSpot虚拟机在编译子系统中加入了分层编译 的功能。

sadasd[注]:分层编译的概念其实很早就已经提出,但直到JDK 6时期才被初步实现,后来一直处于改进阶段,最终在JDK 7的服务端模式虚拟机中作为默认编译策略被开启。

sasd分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括(5层):
在这里插入图片描述
sadasd[注]:分层不是固定不变的。第二层中有限的性能监控功能指的是 开启方法及回边次数统计 等 (它的作用就是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。)

sada改善 :实施分层编译后,解释器、客户端编译器和服务端编译器就会同时工作热点代码都可能会被多次编译,用客户端编译器获取更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行的时候也无须额外承担收集性能监控信息的任务,而在服务端编译器采用高复杂度的优化算法时,客户端编译器可先采用简单优化来为它争取更多的编译时间
sasadasadasadada


热点代码指什么?编译的目标对象是什么?栈上替换(OSR)是什么?

在运行过程中会被即时编译器编译的目标是“热点代码”,这里所指的热点代码主要有两类:
sada
sada①、被多次调用的方法;②、被多次执行的循环体。
sada
sada[注]:对于这两种情况,编译的目标对象都是整个方法体,而不会是单独的循环体。

sadasda①、第一种情况,由于是依靠方法调用触发的编译,那编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的即时编译方式。

sadasda②、对于后一种情况,尽管编译动作是由循环体所触发的,热点只是方法的一部分,但编译器依然必须以整个方法作为编译对象,只是执行入口(从方法第几条字节码指令开始执行)会稍有不同,编译时会传入执行入口点字节码序号(Byte Code Index,BCI)。这种编译方式因为编译发生在方法执行的过程中,因此被很形象地称为“栈上替换”(On Stack Replacement,OSR),即方法的栈帧还在栈上,方法就被替换了。

即时编译被触发的条件分析: “多次调用”、“多次执行”。而"多次"引来的两个问题:

sadaasdasdsda① 、到底多少次才算“多次”呢?sda ②、JVM如何统计某个方法或某段代码被执行过多少次的呢?


对于热点代码,到底多少次才算“多次”呢?

sada要知道某段代码是不是热点代码,是不是需要触发即时编译,这个行为称为“热点探测”,其实进行热点探测并不一定要知道方法具体被调用了多少次,目前主流的热点探测判定方式有两种,分别是:
saaasdasdasdasdasdasdsdda①、基于采样的热点探测;②、基于计数器的热点探测
sada[注]:①、前者周期性的检查各个线程的调用栈顶,如果发现某个(某些)方法经常出现在栈顶,那这个方法就是"热点方法"。
saasdssda优点:实现简单高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可);
saasdssda缺点:很难精确 地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
sadasddda②、对于后者,JVM会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。
saasdssda优点:实现起来要麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系
saasdssda缺点:统计结果相对来说更加精确严谨。
sadddasda③、这两种探测手段在商用Java虚拟机中都有使用到,譬如J9用过第一种采样热点探测,而在HotSpot虚拟机中使用的是第二种基于计数器的热点探测方法。

sada为了实现热点计数,HotSpot为每个方法准备了两类计数器:

saasdasdsadda①、方法调用计数器sada②、回边计数器(“回边”的意思就是指在循环边界往回跳转)

sada当虚拟机运行参数确定的前提下,这两个计数器都有一个明确的阈值,计数器阈值一旦溢出,就会触发即时编译。


方法调用计时器 (统计方法调用的次数)?触发参数?热度衰减?

sa方法调用计时器: 它的默认阈值在客户端模式下是1500次,在服务端模式下是10000次,这个阈值可以通过虚拟机参数-XX CompileThreshold来人为设定。
sadasadasadasadasadasada在这里插入图片描述
sada执行过程:
sadasda①、当一个方法被调用时,虚拟机会先检查该方法是否存在被即时编译过的版本:

sadassdasda⒈如果存在,则优先使用编译后的本地代码来执行。

sadasddasda⒉如果不存在已被编译过的版本,则将该方法的调用计数器值加一,然后判断方法调用计数器与回边计数器值 之和 是否超过方法调用计数器的阈值。
sadadsasddasda㈠、一旦已超过阈值的话,将会向即时编译器提交一个该方法的代码编译请求。
sadadsasddasda㈡、如果没有做过任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被即时编译器编译完成。当编译工作完成后,这个方法的调用入口地址就会被系统自动改写成新值。

sada[注]:
ssaada①、在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。
ssaada②、当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那该方法的调用计数器就会被减少一半,这个过程被称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期
ssaada③、进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样只要系统运行时间足够长,程序中绝大部分方法都会被编译成本地代码。另外还可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒


回边计时器(统计多次执行的循环体)?OSR?

sada作用:统计一个方法中循环体代码执行的次数(在字节码中遇到控制流向后跳转的指令就称为“回边);
saadadadadadaadaddadadad在这里插入图片描述
sada执行过程:
sadasda①、当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本:

sadassdasda⒈如果有的话,它将会优先执行已编译的代码;

sadasddasda⒉否则就把回边计数器的值加一,然后判断方法调用计数器与回边计数器值之和是否超过回边计数器的阈值。当超过阈值的时候,将会提交一个 栈上替换 编译请求,并且把回边计数器的值稍微降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果。

sada[注]:与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。当计数器溢出的时候,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程。

客户端模式虚拟机的即时编译方式,对于服务端模式虚拟机来说,执行情况会比上面描述还要复杂一些。


禁止后台编译参数?后台的编译线程?服务端编译器和客户端编译器的差别?

sada在默认条件下,无论是方法调用产生的标准编译请求,还是栈上替换编译请求,虚拟机在编译器还 未完成编译之前,都仍然将按照 解释方式 继续执行代码,而编译动作则在 后台 的编译线程中进行。用户可以通过参数-XX:-BackgroundCompilation来禁止后台编译。
sada那在后台执行编译的过程中,编译器具体会做什么事情呢?
sada服务端编译器和客户端编译器的编译过程是有所差别的。对于客户端编译器来说,它是一个相对简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。


客户端编译器的三段式编译器?优化完成阶段?HIR?LIR?

sasda在第一个阶段,一个平台独立的前端将字节码构造成一种 高级中间代码 表示(High-Level Intermediate Representation,HIR,即与目标机器指令集无关的中间表示)。HIR使用静态单分配(Static Single Assignment,SSA)的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。在此之前编译器已经会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成
sasda在第二个阶段,一个平台相关的后端从HIR中产生低级中间代码表示(LIR,即与目标机器指令集相关的中间表示),而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式。
sa
sasda最后的阶段是在平台相关的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做 窥孔优化,然后产生机器代码。
aaaaaaaaaaaaaa在这里插入图片描述
sasdasasdasasdasasdasasda在这里插入图片描述


服务端编译器 ? (是一个能容忍很高优化复杂度的高级编译器)

sasda它会执行大部分经典的优化动作,如:无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块重排序(Basic Block Reordering)等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、空值检查消除(Null Check Elimination,不过并非所有的空值检查消除都是依赖编译器优化的,有一些是代码运行过程中自动优化了)等。另外,还可能根据解释器或客户端编译器提供的性能监控信息,进行一些不稳定的预测性激进优化,如守护内联(Guarded Inlining)、分支频率预测(Branch Frequency Prediction)等
sasda服务端编译采用的寄存器分配器是一个全局图着色分配器,它可以充分利用某些处理器架构(如RISC)上的大寄存器集合。以即时编译的标准来看,服务端编译器无疑是比较缓慢的,但它的编译速度依然远远超过传统的静态优化编译器,而且它相对于客户端编译器编译输出的代码质量有很大提高,可以大幅减少本地代码的执行时间,从而抵消掉额外的编译时间开销,所以也有很多非服务端的应用选择使用服务端模式的HotSpot虚拟机来运行。


提前编译器与即时编译器的对比?

aa提前编译有两条分支:

aasad①、与即时编译器类似:在程序运行之前把程序代码编译成机器码的 静态翻译 工作;
aasad[注]:这是传统的提前编译应用形式,它体现出了直指即时编译的最大弱点:即时编译要占用程序运行时间和运算资源。即使现在先进的即时编译器架构有了分层编译的支持,可以先用快速但低质量的即时编译器为高质量的即时编译器争取出更多编译时间,但是,无论如何,即时编译消耗的时间都是原本可用于程序运行的时间,消耗的运算资源都是原本可用于程序运行的资源。但如果是在程序运行之前进行的静态编译,这些耗时的优化就可以放心大胆地进行了.

aasad②、把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码(譬如公共库代码在被同一台机器其他Java进程使用)时直接把它加载进来使用。(已经完全被主流的商用JDK支持)
aasad[注]:
adasasad⒈本质是给即时编译器做缓存加速,去改善Java程序的启动时间,以及需要一段时间预热后才能到达最高性能的问题。这种提前编译被称为动态提前编译(Dynamic AOT)或者索性就大大方方地直接叫即时编译缓存(JIT Caching)
adasasad⒉实际应用起来并不是那么容易,原因是这种提前编译方式不仅要和目标机器相关,甚至还必须与HotSpot虚拟机的运行时参数绑定


提前编译的代码输出质量,一定会比即时编译更高吗?

dsaa提前编译因为没有执行时间和资源限制的压力,能够毫无顾忌地使用 重负载的优化手段,这是一个巨大的优势。但是即时编译器相对提前编译器也有天然优势:(3条)
aa①、性能分析制导优化:(其实就是在编译期无法更准确的收集信息) 在解释器或者客户端编译器运行过程中,会不断收集性能监控信息(譬如某个程序点抽象类通常会是什么实际类型、条件判断通常会走哪条分支、方法调用通常会选择哪个版本、循环通常会进行多少次等),这些数据一般在静态分析时是无法得到的,或者不可能存在确定且唯一的解,最多只能依照一些启发性的条件去进行猜测。但在动态运行时却能看出它们具有非常明显的偏好性。 如果一个条件分支的某一条路径执行特别频繁,而其他路径鲜有问津,那就可以把热的代码集中放到一起,集中优化和分配更好的资源(分支预测、寄存器、缓存等)给它

aa②、激进预测性优化:(其实就是在编译器进行优化要求更加严格,限制更多) 静态优化无论如何都必须保证优化后所有的程序外部可见影响(不仅仅是执行结果)与优化前是等效的,不然优化之后会导致程序报错或者结果不对,即使再快也没有用。而相对于提前编译来说,即时编译的策略就可以不必这样保守,如果性能监控信息能够支持它做出一些正确的可能性很大但无法保证绝对正确的预测判断,就已经可以大胆地按照高概率的假设进行优化,万一真的走到罕见分支上,大不了退回到低级编译器甚至解释器上去执行,并不会出现无法挽救的后果。只要出错概率足够低,这样的优化往往能够大幅度降低目标程序的复杂度,输出运行速度非常高的代码。

aa③、链接时优化:Java语言天生就是动态链接的,一个个Class文件在运行期被加载到虚拟机内存当中,然后在即时编译器里产生优化后的本地代码,这类事情在Java程序员眼里看起来毫无违和之处。但如果类似的场景出现在使用提前编译的语言和程序上,就会出现很明显的边界隔阂,还难以优化。


后端优化之方法内联?虚方法?CHA技术?激进?

aa它是编译器最重要的优化手段。内联被业内戏称为优化之母,意义:

aasdasdasdasasda①、消除方法调用的成本; ②为其他优化手段建立良好的基础

aa行为: 把目标方法的代码原封不动地“复制”到发起调用的方法之中,避免发生真实的方法调用。

aa难点:

adsaa对于方法调用,只有使用invokespecial指令调用的私有方法、实例构造器、父类方法、final修饰的方法和使用invokestatic指令调用的静态方法才会在编译期进行解析。而除了这5种 “非虚” 方法,其它的"虚"方法调用都必须在运行时进行方法接收者的多态选择,它们都有可能存在多于一个版本的方法接收者。而最普遍的方法是虚方法。

adsaa对于一个虚方法,编译器静态地去做内联的时候很难确定应该使用哪个方法版本。因为方法版本是根据实际类型动态分派的,而实际类型必须在实际运行到这一行代码时才能确定,编译器很难在编译时得出绝对准确的结论。

adsaa更糟糕的情况是,由于Java提倡使用面向对象的方式进行编程,而Java对象的方法默认就是虚方法,可以说Java间接鼓励了程序员使用大量的虚方法来实现程序逻辑。根据上面的分析可知,内联与虚方法之间会产生“矛盾”,那是不是为了提高执行性能,就应该默认给每个方法都使用final关键字去修饰呢?(C#是这样做的,默认的方法是非虚方法,如果需要用到多态,就用virtual关键字来修饰,但Java选择了在虚拟机中解决这个问题)
aa
a如何解决虚方法的内联问题呢?
aa
ssa JVM首先引入了一种名为类型继承关系分析(CHA)的技术这是整个应用程序范围内的 类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。

ssa根据CHA,编译器在进行内联时就会分不同情况采取不同的处理:

ssadas⒈如果是非虚方法,那么直接进行内联就可以了,这种的内联是有百分百安全保障的;

ssadas⒉如果遇到虚方法,则会向CHA查询此方法在当前程序状态下是否真的有多个目标版本可供选择:

ssaddasasⅰ 如果查询到只有一个版本,那就可以假设“应用程序的全貌就是现在运行的这个样子”来进行内联,这种内联被称为 守护内联.
ssadas[注]:由于Java程序是动态连接的,说不准什么时候就会加载到新的类型从而改变CHA结论,因此这种内联属于激进预测性优化,必须预留好“逃生门”,即当假设条件不成立时的“退路”(Slow Path)。假如在程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接收者的继承关系发生变化的类,那这个内联优化的代码就可以一直使用下去。如果加载了导致继承关系发生变化的新类,那么就必须抛弃已经编译的代码,退回到解释状态进行执行,或者重新进行编译

ssaddasasⅱ 如果该方法确实有多个版本的目标方法可供选择,那即时编译器还将进行最后一次努力,使用 内联缓存的方式来缩减方法调用的开销。这种状态下方法调用是真正发生了的,但是比起直接查虚方法表还是要快一些。

ssadas[注]:内联缓存是一个建立在目标方法正常入口之前的缓存,它的工作原理大致为:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较接收者的版本:

ssadas⒈如果以后进来的每次调用的方法接收者版本都是一样的,那么这时它就是一种单态内联缓存。通过该缓存来调用,比用不内联的非虚方法调用,仅多了一次类型判断的开销而已。

ssadas⒉如果真的出现方法接收者不一致的情况,就说明程序用到了虚方法的多态特性,这时候会退化成超多态内联缓存,其开销相当于真正查找虚方法表来进行方法分派。
在这里插入图片描述
a定性分析:
aa
ssa 在多数情况下Java虚拟机进行的方法内联都是一种 激进优化。事实上,激进优化的应用在高性能的Java虚拟机中比比皆是,极为常见。除了方法内联之外,对于出现概率很小(通过经验数据或解释器收集到的性能监控信息确定概率大小)的隐式异常、使用概率很小的分支等都可以被激进优化“移除”,如果真的出现了小概率事件,这时才会时从“逃生门”回到解释状态重新执行。


后端优化之逃逸分析?栈上分配?标量替换?同步消除?无效代码消除?

adsa逃逸分析是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术
aa
asa基本原理:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
在这里插入图片描述
asa如果能证明一个对象不会逃逸到方法或线程之外,则可能为这个对象实例采取不同程度的优化,如下:
asssa栈上分配 (OSR): 如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。

assssa[注]:栈上分配可以支持方法逃逸,但不能支持线程逃逸
asa
asssa标量替换:

asdssa 首先介绍两个概念:

assadassa①、若一个数据已经无法再分解成更小的数据(int、long、reference等原始数据类型类型)来表示了,那么这些数据就可以被称为标量.

assadassa②、如果一个数据可以继续分解(Java),那它就被称为聚合量.

asdssa 如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为 标量替换。 假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。

assssa[注]:标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。
asa
asssa同步消除: 线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量 实施的同步措施也就可以安全地消除掉。

a定性分析:
aa
ssa逃逸分析的计算成本非常高,甚至不能保证逃逸分析带来的性能收益会高于它的消耗。如果要百分之百准确地判断一个对象是否会逃逸,需要进行一系列复杂的数据流敏感的过程间分析,才能确定程序各个分支执行时对此对象的影响。
ssa尽管目前逃逸分析技术仍在发展之中,未完全成熟,但它是即时编译器优化技术的一个重要前进方向,在日后的Java虚拟机中,逃逸分析技术肯定会支撑起一系列更实用、有效的优化技术。

a举例(重要):

//步骤0:完全未优化的代码
public int test(int x) {  
	int xx = x + 2;
	Point p = new Point(xx, 42);
	return p.getX();
}
//步骤1,将Point的构造函数和getX()方法进行内联优化:
public int test(int x) {		
	int xx = x + 2;
	Point p = point_memory_alloc(); // 在堆中分配P对象的示意方法
	p.x = xx; // Point构造函数被内联后的样子
	p.y = 42
	return p.x; // Point::getX()被内联后的样子
}
// 步骤2:标量替换后的样子
public int test(int x) {
	int xx = x + 2;
	int px = xx;
	int py = 42
	return px;
}
// 步骤3:做无效代码消除后的样子
public int test(int x) {
	return x + 2;
}

后端优化之公共子表达式消除

adsa公共子表达式消除是一项非常经典的、普遍应用于各种编译器的优化技术.
aa
adsa什么叫公共子表达式呢? 如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式
adsa对于这种表达式,没有必要花时间再对它重新进行计算,只需要直接用前面计算过的表达式结果代替E。如果这种优化仅限于程序基本块内,便可称为局部公共子表达式消除,如果这种优化的范围涵盖了多个基本块,那就称为全局公共子表达式消除.

adsa举例:

int d = (c * b) * 12 + a + (a + b * c);

adsa当这段代码进入虚拟机即时编译器后,它将进行如下优化:编译器检测到cb与bc是一样的表达式,而且在计算期间b与c的值是不变的。

int d = E * 12 + a + (a + E);

adsa编译器还可能——代数化简

int d = E * 13 + a + a;

后端优化之数组边界检查消除 ?隐式异常处理 ?自动装箱消除?安全点消除?、消除反射?

adsa是即时编译器中的一项语言相关的经典优化技术。
aa
adsaJava语言是一门动态安全的语言,对数组的读写访问也不像C、C++那样实质上就是裸指针操作。每次对数组访问时,系统都会自动进行上下界的范围检查。这对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这必定是一种性能负担。
adsa无论如何,为了安全,数组边界检查肯定是要做的,但数组边界检查是不是必须在运行期间一次不漏地进行则是可以“商量”的事情。
adsa如果我们在编译器根据数据流分析就确定了要访问的数组的长度,并且该长度没有越界,则执行的时候就可以把整个数组的上下界检查消除掉,这可以节省很多次的条件判断操作。
assssa[注]:
assssa大量的安全检查使编写Java程序比编写C和C++程序容易了很多,但因此Java要比C#做更多的事情(各种检查判断),些事情就会导致一些隐式开销,如果不处理好它们,就很可能成为一项“Java语言天生就比较慢”的原罪。
assssa为了消除这些隐式开销,除了如数组边界检查优化这种尽可能把运行期检查提前到编译期完成的思路之外,还有一种避开的处理思路——隐式异常处理,Java中空指针检查和算术运算中除数为零的检查都采用了这种方案。比如:

if (foo != null) {
	return foo.value;
}else{
	throw new NullPointException();
}

assssa在使用隐式异常优化之后,虚拟机会把上面的伪代码所表示的访问过程变为如下伪代码:

try {
	return foo.value;
} catch (segment_fault) {
	uncommon_trap();
}

assssaVM会注册一个Segment Fault信号的异常处理器(伪代码中uncommon_trap(),务必注意这里是指进程层面的异常处理器,并非真的Java的try-catch语句的异常处理器),这样当foo不为空的时候,对value的访问是不会有任何额外对foo判空的开销的,而代价就是当foo真的为空时,必须转到异常处理器中恢复中断并抛出NullPointException异常。进入异常处理器的过程涉及进程从用户态转到内核态中处理的过程,结束后会再回到用户态,速度远比一次判空检查要慢得多

adsa与语言相关的其他消除操作还有不少,如自动装箱消除(AutoboxElimination)、安全点消除(Safepoint Elimination)、消除反射(Dereflection)等.

JVM中线程有什么?
JVM线程

adsa1、线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行

adsa2、在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射

adsa3、当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收
操作系统负责将线程安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run( )方法

adsa4、如果一个线程抛异常,并且该线程是进程中最后一个守护线程,那么进程将停止

JVM系统线程

adsa如果你使用 jconsole 或者是任何一个调试工具,都能看到在后台有许多线程在运行。

adsa这些后台线程不包括调用 public static void main(String [ ])的main线程以及所有由这个main方法自己创建的线程。

adsa这些主要的后台系统线程在Hotspot JVM里主要是以下几个:

adsasa虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
aaddsa①、周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行
adaasa②、GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持(重点)
adsasa③、编译线程:这种线程在运行时会将字节码编译成 本地代码
adadsa④、信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理

调优参数

adsa堆栈内存相关

-Xms 设置初始堆的大小
-Xmx 设置最大堆的大小
-Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
-Xss 每个线程的堆栈大小
-XX:NewSize 设置年轻代大小(for 1.3/1.4)
-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
-XX:NewRatio 年轻代与年老代的比值(除去持久代)
-XX:SurvivorRatio Eden区与Survivor区的的比值
-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
-XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代

adsa 垃圾收集器相关

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20:配置并行收集器的线程数
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是
可以消除碎片

adsa 辅助信息相关

-XX:+PrintGCDetails 打印GC详细信息
-XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用
-XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题.
-XX:+PrintTLAB 查看TLAB空间的使用情况

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐