读书笔记-深刻理解Java虚拟机

概述

深刻理解Java虚拟机:JVM高级特性与最佳实践(第三版)周志明 读书笔记选取了书中部份内容,Java虚拟机更多的是一种规范,具体的Java虚拟机实现是有不少的。做者提到本文多数是以Hotspot虚拟机做为讲解。html

第二部分 自动内存管理

第2章 Java内存区域与内存溢出异常

运行时数据区:计数器,Java虚拟机栈(局部变量表,操做数栈)方法区(类型信息,运行时常量池
等), Java堆,直接内存。可能产生内存溢出场景。虚拟机对象建立,内存布局与访问定位,前端

第3章 垃圾收集与内存分配策略

强引用,软引用,弱引用,虚引用,分代收集,并发可达性分析,标记-清除,标记-复制,标记-整理,java

第三部分 虚拟机执行子系统

第6章 类文件结构

Class类文件结构,字节码指令集程序员

第7章 虚拟机类加载机制

加载,连接,初始化,类加载器,双亲委派模型算法

第8章 虚拟机字节码执行引擎

运行时栈结构,方法解析分派,动态类型语言支持,基于栈的字节码执行数据库

第四部分 程序编译与代码优化

第10章 前端编译与优化

从Java文件到字节码阶段:Java注解处理器,泛型,装拆箱,插入式注解处理器编程

第11章 后端编译与优化

从字节码到机器码:AOT,JIT , Android Dalvik,Android ART后端

第2章 Java内存区域与内存溢出异常

2.1 概述

对于C/C++程序员来讲,担负每个对象生命从开始到终结的维护责任。而对于Java程序员来讲,Java帮助程序员自动管理内存,不须要写显式的代码去释放内存。但虚拟机不是万能的,一旦出现内存泄漏和溢出问题,若是不了解虚拟机怎样使用内存,将难以排查错误和修正问题。 本章从概念上介绍Java虚拟机内存的各个区域,及其可能产生的问题。数组

2.2 运行时数据区域

Java虚拟机在执行Java程序时,会将它管理的内存分红功能不一样的运行时数据区域。这些区域有着不一样的用户,不一样的建立和销毁时间。 有的区域随着虚拟机进程生命周期,有的区域则依赖用户线程的启动和结束而创建和销毁。根据《Java虚拟机规范》规定,Java虚拟机管理的内存包括如下运行时数据区。缓存

截屏2020-08-04下午8.07.42.png Java虚拟机基于栈的方式去执行程序。每个线程都会有相应的虚拟机栈,而虚拟机栈的栈帧对应于Java方法。

2.2.1 程序计数器

程序计数器(Progrom Counter Register),记录当前线程执行的字节码行号指示器。一般来讲,字节码解释器工做时就是经过改变计数器的值来选取下一条须要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等都依赖计数器完成。每一个线程都有独立的计数器,互不影响。计数器分配在线程私有的内存空间中。此区域不会出现OOM的状况。
复制代码

2.2.2 Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程相同,描述的是Java方法执行的线程内存模型,即每一个方法被执行时,都会同步建立一个栈帧(Stack Frame) 存储局部变量表,操做数栈,动态链接等。方法的调用与执行完毕,对应一个栈帧的入栈和出栈。
复制代码

截屏2020-08-05下午9.03.19.png 在Java源码编译成字节码时,一个栈帧须要多大的局部变量表,须要多深的操做数栈就已经被分析计算出来,即编译事后就已经可以须要多大内存,内存取决于源码和具体的虚拟机栈内存布局形式。 在《Java虚拟机规范》中提到:若是线程请求的栈深度大于虚拟机所容许的深度,将会抛出StackOverflowError异常;若是虚拟机栈容量能够动态扩展,无限扩展,内存不足会抛出OutOfMemoryError异常。 Hotspot虚拟机栈容量不可动态扩展,但若是线程申请栈空间失败,仍然会OOM。

2.2.3 本地方法栈

相较于Java虚拟机栈执行Java方法,本地方法栈执行Native方法。做用是类似的,也会有一样的异常问题。在HotSpot虚拟机中,本地方法栈与Java虚拟机栈合二为一。
复制代码

2.2.4 Java堆

Java堆是全部线程共享的内存区域,在虚拟机启动时建立。用来存放对象实例。数组也是一种对象实例。
 Java堆是垃圾收集器管理的内存区域。基于分代收集理论设计,多数虚拟机的堆内存能够分为新生代、老年代、永久代,Eden,Survivor等。随之垃圾收集器技术的发展,也出现了不采用分代设计的新垃圾收集器,那就不存在上述所谓的代划分。![截屏2020-08-05下午3.27.29.png](https://cdn.nlark.com/yuque/0/2020/png/1305846/1596612457012-03fbf28c-2cf4-43be-a24f-7144ada446a4.png#align=left&display=inline&height=155&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2020-08-05%E4%B8%8B%E5%8D%883.27.29.png&originHeight=166&originWidth=729&size=15139&status=done&style=none&width=680)
复制代码

Java堆中,能够划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)。咱们说TLAB是现成独享的,但只是分配是独享的,读操做和垃圾回收等动做上是线程共享的。TLAB一般是在Eden区分配,由于Eden区自己不大,TLAB实际内存也很是小,默认占Eden空间的1%,因此必然存在一些大对象没法在TLAB直接分配。 截屏2020-08-05下午9.02.34.png 不管怎么划分,都不会改变堆存放对象实例的做用。各类划分是为了更好的分配和回收内存。 《Java虚拟机规范》规定,逻辑上连续的内存空间,在物理上能够不连续。但多数虚拟机实现出于实现简单和存储高效,也会要求连续的物理内存空间。 主流Java虚拟机的堆内存空间都是可扩展的,但有上限值。当对象实例没法被分配内存,且堆达到上限。Java虚拟机便会抛出OutOfMemoryError异常。

2.2.5/2.2.6 方法区(包含运行时常量池)

方法区(Method Area),运行时常量池(Runtime Constant Pool)
复制代码

方法区,线程共享,存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。 JDK8,再也不使用永久代(Permanent Generation Space)实现方法区,而是在本地内存中实现的元空间(Metaspace)来代替。而字符串常量移到Java堆。这部分的内存回收目标主要针对常量池的回收和对类型的卸载。 截屏2020-08-05下午3.22.27.png 运行时常量池,存放常量池表(Constant Pool Table),即Class文件编译期生成的各类字面量与符号引用。 根据《Java虚拟机规范》规定,若是方法区没法知足新的内存分配,将会抛出OutOfMemoryError异常。

2.2.7 直接内存

直接内存(Direct Memory). NIO(New input/output)是 JDK1.4新加入的类,引入了一种基于通道(channel)和缓冲区(buffer)的I/O方式,它可使用Native函数直接分配堆外内存,而后经过堆上DirectByteBuffer对象对这块内存进行引用和操做。直接内存的大小不受JVM的限制,但一样可能会OutOfMemoryError异常。 截屏2020-08-05下午9.01.53.png

2.3 HotSpot虚拟机对象探秘

以HotSpot为例,讲述在Java堆中对象的建立、结构和访问。

2.3.1 对象建立截屏2020-08-05下午9.00.56.png

分配堆内存 根据内存是否规整,分为两种:指针碰撞(Bump The Pointer)和空闲列表(Free List).前者内存规整。 截屏2020-08-05下午8.59.51.png 解决并发状况下的线程安全问题的两种方式

  • 对分配内存的动做进行同步处理,实际采用CAS(CompareAndSwap)配上失败重试保证操做的原子性
  • 本地线程缓冲区(Thread Local Allocation Buffer,TLAB,线程预分配私有写内存区域。

2.3.2对象结构

  • 对象头(Header)
    • 对象自身运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID,偏向时间戳等。这部分的数据在32位虚拟机和64位虚拟机上的大小分别也是32bit和64bit,称之为Mark Word。
    • 类型指针,指向其类型元数据。
  • 实例数据(Instance Data)
    • 对象真正存储的有效信息
  • 对齐填充(Padding)
    • 保证对象内存大小为8字节的整数倍,对齐填充补全。

2.3.3对象的访问定位

在《Java虚拟机规范》中规定,栈上的reference类型数据只是一个指向对象的引用。实际经过引用访问对象有两种方式:

  • 句柄访问,reference存储的是句柄地址,对象被移动,改变句柄中的指针就好,reference自己不会被修改。
  • 直接指针访问,少一次指针定位的开销,对象访问在Java中很是频繁。HotSpot采用直接指针访问。

截屏2020-08-06上午11.40.49.png截屏2020-08-06下午1.14.42.png

2.4 实战:OutOfMemoryError异常

模拟Java堆、虚拟机栈、本地方法栈、方法区、运行时常量池,本地直接内存的溢出。

2.4.1 Java堆溢出

/***Intellij IDEA 配置 Run Configgurations * VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * 限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为同样) */ 
public class HeapOOM {
    static class OOMObject{}
    public static void main(String[] args) {
        List<OOMObject> list=new ArrayList<OOMObject>();
        while (true){
            list.add(new OOMObject());
        }
    }
}
复制代码

运行结果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid58651.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [27770272 bytes in 0.159 secs]
复制代码

2.4.2 虚拟机栈和本地方法栈溢出

HotSpot不区分虚拟机栈和本地方法栈,须要设置 -Xss。 -Xoss(设置本地方法栈)没有效果。
复制代码

两种异常:

  • 栈内存不可动态扩展,请求栈深度大于容许最大深度,则 StackOverflowError。
  • 栈内存可动态扩展,当内存不足,没法申请,则 OutOfMemoryError。

HotSpot栈内存不容许动态扩展,咱们使用-Xss参数减小栈内存容量。

// VM Args:-Xss160k
public class JavaVMStackOF {
    private int stackLength = 1;
    
    public void stackLeak() {
        stackLength++;
        System.out.println("stack length:" + stackLength);
        stackLeak();
    }
    public static void main(String[] args) throws Exception {
        JavaVMStackOF oom = new JavaVMStackOF();
        try {
            oom.stackLeak();
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }
}
复制代码

运行结果

...
stack length:754
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:9)
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:10)
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:10)
...
复制代码

2.4.3 方法区和运行时常量池溢出

截屏2020-08-06下午4.11.39.png

在JDK6及以前版本中运行String::intern()

在JDK6及之前的HotSpot中,常量池分配在永久代中,经过 -XX:PermSize 和 -XX:MaxPermSize限制永久代的大小。String::intern() 能够将一个字符串对象添加到常量池,若是常量池中不包此字符串对象。 在JDK6的HotSpot中

/*** VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M */ 
public class RuntimeConstantPoolOOM { 
    public static void main(String[] args) {
        // 使用Set保持着常量池引用,避免Full GC回收常量池行为 
        Set<String> set = new HashSet<String>(); 
        // 在short范围内足以让6MB的PermSize产生OOM了 
        short i = 0; 
        while (true) { 
            set.add(String.valueOf(i++).intern()); 
        } 
    } 
}
复制代码

运行结果,也验证了字符串常量在永久代中

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 
at java.lang.String.intern(Native Method) 
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18
复制代码

在JDK7及之后版本中运行String::intern()

因为字符串常量转移到了Java堆中, 因此在JDK7中设置 -XX:MaxPermSize, 或者在JDK8中设置 --XX:MaxMeta-spaceSize都不会出现JDK6中的溢出问题。但咱们能够限制最大堆内存空间-Xmx6m,从而产生OOM。

/*** VM Args:-Xmx6m */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }
}
复制代码

运行结果,也验证了字符串常量在堆中

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.HashMap.put(HashMap.java:611)
	at java.util.HashSet.add(HashSet.java:219)
	at oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:27)
复制代码

2.4.4 本机直接内存溢出

直接内存(Direct Memory)的容量大小可经过 -XX:MaxDirectMemorySize指定。

/*** VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M */
public class DirectMemoryOOM {

    public static void main(String[] args) throws Exception {
        int count=1;
        Field unsafeFiled=Unsafe.class.getDeclaredFields()[0];
        unsafeFiled.setAccessible(true);
        Unsafe unsafe= (Unsafe) unsafeFiled.get(null);
        while (true){
            unsafe.allocateMemory(1024*1024*1024);
            System.out.println(count++);
        }
    }
}
复制代码

运行结果

Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at oom.DirectMemoryOOM.main(DirectMemoryOOM.java:17)
复制代码

2.5 小结

到目前为止,明白了虚拟机中内存的划分,以及出现内存溢出的场景。下一章将详细讲解Java垃圾收集机制如何避免内存溢出。

第3章 垃圾收集与内存分配策略

3.1概述

垃圾收集(Garbage Collection,GC),1960年麻省理工Lisp语言,使用动态内存分配和垃圾收集技术。 当Lisp胚胎时期,其做者John McCarthy就思考过GC须要完成的三件事情:

  • 哪些内存须要回收
  • 何时回收
  • 如何回收

Java虚拟机内存运行时区间中,程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭。在编译器其大小基本肯定。而GC关注的是线程共享的区域 Java堆和方法区。

3.2对象已死?

GC回收堆以前,如何断定哪些对象须要被回收,或者说这些对象已死?截屏2020-08-12下午8.27.10.png

3.2.1 引用计数法(Reference Counting)

3.2.2可达性分析算法(Reachability Analysis)

可固定做为GC Roots的对象包括:

  • 虚拟机栈帧中的本地变量表引用的对象
  • 方法区中类静态属性引用对象
  • 方法区中常量引用对象
  • 本地方法栈中JNI引用对象
  • 虚拟机内部的引用,如Class对象,常驻异常对象(NullPointException),类加载器等等
  • 被同步锁持有的对象
  • 反映Java虚拟机内部状况的JMXBean,JVMTI中注册的回调,本地代码缓存等。

除固定外,还有临时性的其余对象等。

3.2.3再谈引用

Java将引用分为四种:

  • 强引用(Strongly Reference),引用赋值(即Object obj=new Object),永不回收
  • 软引用(Soft reference),存活到即将发生内存溢出异常前的二次回收
  • 弱引用(Weak Reference),存活到下一次垃圾收集
  • 虚引用(Phantom Reference),没法经过虚引用获取对象实例,只是感知对象被回收

3.2.4 生存仍是死亡

要宣告一个对象死亡,至少要经历两次不可达标记过程。重载对象的finalize()方法,能够再第一次标记后执行,从新挂上引用链避免被回收,但finalize()只会被执行一次。

3.2.5回收方法区

3.3垃圾收集算法

  • 引用计数式垃圾收集(Reference Counting GC)
  • 追踪式垃圾收集(Tracing GC)

这里咱们讨论的是追踪式垃圾收集。

3.3.1 分代收集理论

分代收集(Generational Collection)的理论假说基础:

  • 弱分代假说(Weak Generational Hypothesis)

绝大多数对象都是朝生夕灭的

  • 强分代假说(Strong Generational Hypothesis):

熬过越屡次垃圾收集过程的对象越难以消亡

  • 跨代引用假说(Intergenerational Reference Hypothesis):

跨代引用相对同代引用来讲,仅占极少数

基于弱分代/强分代假说,通常Java虚拟机至少会把Java堆划分为

  • 新生代(Young Generation)
  • 老年代(Old Generation)

每次垃圾收集,新生代大量死去的对象会被回收,存活的对象将会逐步晋升到老年代中。大对象直接进入老年代。在新生代中创建一个全局的数据结构(记忆集,Remebered Set),把老年代分红多个小块,标识出某些块存在跨代引用。 针对不一样级别的分代的收集,咱们定义一下名词:

  • 部分收集(Partial GC)
    • 新生代收集(Minor GC/Young GC)
    • 老年代收集(Major GC/Old GC)
    • 混合收集(Mixed GC),收集新生代和部分老年代
  • 整堆收集(Full GC)

3.3.2 标记-清除算法

标记-清除(Mark-Swap) 1960 Lisp John McCarthy,基础性算法。 缺点:

  • 执行效率不稳定,随对象数量增加而下降
  • 内存碎片化问题

截屏2020-08-13下午3.26.28.png

3.3.3 标记-复制算法

1969 Fenichel "半区复制" SemisSpace Copying 解决内存碎片化和执行效率问题,缺点明显:浪费一半内存。

截屏2020-08-13下午4.36.59.png IBM研究发现98%的新生代对象熬不过第一轮收集,所以不用1:1分配内存 1989年 Andrew Appel ,提出更优化的半区复制分代策略。 能够看到始终有一个 Survivor(10%新生代内存)做为保留,用来存放回收后存活对象,若果Survivor空间不够,则须要老年代进行分配担保(Handle Promotion) 截屏2020-08-13下午5.17.17.png 标记-复制算法适用于新生代,即大量对象会被回收,须要复制的对象不多。老年代对象存活率高,就不适用了。

3.3.4 标记-整理算法

标记-整理(Mark-Compact) 1974年 Edward Lueders。 移动式回收算法,标记-清除是非移动式的。 移动对象则回收时复杂,不移动对象则分配内存时复杂。截屏2020-08-13下午5.47.06.png

3.4 HotSpot垃圾收集算法细节

3.4.1 根节点枚举

全部垃圾回收算法在根节点枚举时,都须要暂停用户线程,即 Stop The World! HotSpot采用准确式(Exact)垃圾回收,使用称为OopMap的数据结构,记录栈上本地变量到堆上对象的引用关系。 从而减小根节点枚举耗费的大量时间。 找出栈上的指针/引用 介绍了保守式,半自动式,准确式垃圾回收,同时也引出了OopMap。

3.4.2 安全点

根节点枚举须要暂停线程,总不能在每条指令后都去中断线程,因此有些固定的指令位置,做为中断的点,称之为 safe point。采用主动式中断,即达到安全点,检查是否要执行中断线程。 安全点的位置通常为:

  • 循环的末尾
  • 方法返回前/调用方法的call指令后
  • 可能抛出异常的位置

3.4.3 安全区域

safe region。安全区域指在某个代码片断中,引用关系不会发生变化,在这个区域内能够安全的开始垃圾收集。截屏2020-08-17下午11.26.17.png

3.4.4 记忆集与卡表

RememberSet,记录非收集区到收集区的引用,避免把整个非收集区加入到GC Root扫描。好比说收集新生代对象时,避免整个老年代加入GCRoot扫描。
复制代码

从精度上来看,记忆集能够分为

  • 字长精度
  • 对象精度
  • 卡精度,每一个记录精确到一块内存区域,记录该区域内是否含有跨代指针。

卡表,即为常见的卡精度的记忆集。 卡表简单来讲,能够只是一个字节数组(Card Table)。每一个数组元素都对应一个卡页(Card Page),卡页是某块特定大小的内存块,通常来讲大小为2的N次幂字节数,HotSpot中为512字节。只要卡页中有对象存在跨代引用,则对应卡表元素标记为1,即元素变脏(Dirty)截屏2020-08-18上午11.22.24.png

3.4.5 写屏障与AOP

什么时候去记录RememberSet? 写屏障(Write Barrier),虚拟机层面对"引用类型字段赋值"动做的AOP切面,虚拟机为赋值操做生成相应指令。 环形(Around)通知,提供写前屏障(Pre-Write Barrier)和写后屏障(Post-Write Barrier)。

假设处理器的缓存行大小为64字节,因为一个卡表元素占1个字节,64个卡表元素将共享同一个缓 存行。卡表在高并发下的伪共享(False Sharing)问题, 写脏前,先判断是否已脏。 在JDK 7以后,HotSpot虚拟机增长了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断

3.4.6 并发的可达性分析与三色标记

前面咱们经过OopMap、安全区域、RememberSet等手段,提高了根节点枚举的速度。根节点枚举带来的停顿已经至关短暂和固定了,而从GC Roots继续往下遍历对象的停顿时间与堆容量成正比。 可达性分析(标记)算法要求全过程在一个一致性的快照中分析,势必要冻结所有用户线程,且冻结的时间彻底不可控。在堆容量过大状况下,冻结时间是没法接受的。所以,可达性分析过程,若是能与用户线程并发执行,是最好不过了。 咱们先来看并发可达性分析过程 即三色标记 截屏2020-08-18下午3.54.48.png 并发可达性分析又会引发两类问题:

  • 1.该回收的没有被标记(浮动垃圾,Floating Garbage),这个可接受,大不了下次回收时,再回收。
  • 2.不应回收的被标记(对象消失),这个不可接受,由于用户线程须要的对象没了。

当且仅当同时知足下面两个条件,会产生对象消失问题,即本来应当为黑色的对象被误标为白色(Wilson,1994年证实):

  • 赋值器插入了一条黑色到白色的引用。
  • 同时赋值器删除了所有灰色到该白色的直接或间接引用。

截屏2020-08-18下午3.55.45.png

  • 增量更新,记录新增的引用,并发扫描结束后,从新以黑色为根扫描,即黑色变为灰色
  • 原始快照,记录删除的引用,并发扫描结束后,从新以灰色为根扫描。

3.5 经典垃圾收集器

3.5.0 概述截屏2020-08-18下午5.49.49.png

在介绍以前咱们先明确几个概念:

  • 并行,能够有多个垃圾收集线程同时运行。 串行则同时只能有一个垃圾收集线程运行。
  • 并发,垃圾收集线程能够与用户线程同时运行。
  • 高吞吐量,垃圾收集时间/(用户线程运行时间+垃圾收集时间)
  • 低延迟,快速响应。能够容忍总的收集时间增长,下降平均每次收集时间。

3.5.1 Serial收集器

新生代,无并行,无并发,标记复制,简单高效,额外内存消耗(Memory Footprint)最小,适用于单核/少核,JDK1.3.1以前。截屏2020-08-19下午1.17.22.png

3.5.2 ParNew收集器

新生代,并行,Serial的多线程版本,标记复制, [JDK1.3 - JDK8)

3.5.3 Parallel Scavenge收集器

新生代,并行 ,无并发,JDK1.4,标记复制,注重吞吐量截屏2020-08-19下午1.17.40.png

3.5.4 Serial Old 收集器

老年代,无并行,无并发,Serial老年代版本,标记整理

3.5.5 Parallel Old 收集器

老年代,并行,无并发,Parallel Scavenge 老年代版本,标记整理,注重吞吐量

3.5.6 CMS收集器

Concurent Mark Sweep, 老年代,并行,并发, [JDK5 - JDK8],标记清除,注重低延迟, 并发标记使用增量更新。四个步骤:

    1. 初始标记(CMS initial mark),仅标记GC Roots直接关联对象,短STW
  • 2)并发标记(CMS concurrent mark),遍历整个对象图,耗时长,能够与用户线程并发
  • 3)从新标记(CMS remark),增量更新,避免对象消失问题,短STW
  • 4)并发清除(CMS concurrent sweep),不须要移动对象,能够与用户线程并发

截屏2020-08-18下午10.58.20.png CMS的三个明显缺点:

  • 回收线程占用处理器资源,CMS默认启动的回收线程数 (处理器核心数量+3)/4. 并发阶段,应用程序会变慢,吞吐量下降
  • 浮动垃圾 Floating Garbage, 并发过程失败,须要启用Serial Old,作一次老年代收集。
  • 内存碎片,标记清除算法带来的问题,进行若干次标记清除后,会执行一次碎片整理。由于整理须要移动对象,没法并发。

3.5.7 G1收集器

Garbage First,JDK7完善, JDK9开始成为默认垃圾收集器。 新生代,老年代。Region分区,局部标记-复制,总体标记-整理,注重低延迟,并发标记使用原始快照。 大对象(内存超过Region内存的一半)直接进入 Humongous Region区域。Region是回收的最小单元。每个Region均可以根据须要扮演Eden空间,Survivor空间或者老年代空间。可预测时间停顿模型。 四个步骤:

    1. 初始标记( Initial Marking),仅标记GC Roots直接关联对象,短STW
  • 2)并发标记(Concurrent Marking),遍历整个对象图,耗时长,能够与用户线程并发
  • 3)最终标记(Final Marking),并发标记使用原始快照,避免对象消失问题,短STW
  • 4)筛选回收(Live Data Counting and Evacuation),对各个Region回收的价值和成本排序,根据指望停顿时间,组合任意多个Region回收。待回收的Region存活对象复制到空Region中,回收旧Region,涉及对象移动,须要STW

截屏2020-08-18下午10.59.27.png G1是垃圾收集器技术发展历史上的里程碑式的结果,开创了面向局部手机的设计思路和基于Region的内存布局形式。从G1开始,垃圾收集器的设计导向变为追求应付内存分配速率(Allocation Tate),而不追求一次把整个Java堆清理干净。G1的更多介绍

3.6 低延迟垃圾收集器

3.6.1 Shenandoah收集器

略,实在不会

3.6.2 ZGC收集器

略,实在不会

第6章 类文件结构

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

6.1 概述

程序语言 -->  字节码 -->  二进制本地机器码

6.2 无关性的基石

无关性的基石  --  字节码(Byte Code) 平台无关性,语言无关性截屏2020-08-19下午4.11.19.png

6.3 Class类文件的结构

Class文件以8个字节为单位的二进制流,各数据项严格按照顺序紧凑排列在文件中,中间没有任何分隔符。 Class文件结构中只有两种数据类型:

  • 无符号数,基本数据类型,u一、u二、u四、u8分别表明1个字节 、2个字节、4个字节 、8个字节的无符号数,用来描述数字,引用,数量值或者UTF-8编码字符串。
  • 表,多个无符号数组成的符合数据类型,命名通常以“_info”结尾。
类型 名称 数量 解释
u4 magic 1 4字节魔数 0xCAFEBABE
u2 minor_version 1 次要版本号
u2 major_version 1 主要版本号
u2 constant_pool_count 1 常量池计数值
cp_info constant_pool constant_pool_count-1 常量池
u2 access_flags 1 访问标志
u2 this_class 1 类索引
u2 super_class 1 父类索引
u2 interfaces_count 1
u2 interfaces interfaces_count 接口索引集合
u2 fields_count 1
field_info fields fields_count 字段表集合,类变量
u2 methods_count 1
method_info methods methods_count 方法表集合
u2 attributes_count 1
attribute_info attributes attributes_count 属性表集合

先来一段代码 TestClass.Java

package clazz;
public class TestClass {
    public static void main(String[] args) { }
    private int m;
    public int inc(){return m+1;}
}
复制代码

经过编译获得二进制字节码文件 TestClass.class 截屏2020-08-19下午5.20.20.png

javap -v TestClass.class获得字节码中包含的类信息。咱们如今要作的就是模拟javap这个解析的过程。

Last modified 2020-7-29; size 483 bytes
  MD5 checksum ad62060802ee27c385e20042d24e8b38
  Compiled from "TestClass.java"
public class clazz.TestClass minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref          #4.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#23         // clazz/TestClass.m:I
   #3 = Class              #24            // clazz/TestClass
   #4 = Class              #25            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lclazz/TestClass;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               inc
  #19 = Utf8               ()I
  #20 = Utf8               SourceFile
  #21 = Utf8               TestClass.java
  #22 = NameAndType        #7:#8          // "<init>":()V
  #23 = NameAndType        #5:#6          // m:I
  #24 = Utf8               clazz/TestClass
  #25 = Utf8               java/lang/Object
{
  public clazz.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lclazz/TestClass;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lclazz/TestClass;
}
SourceFile: "TestClass.java"
复制代码

6.3.1 魔数与Class文件版本

8C05AAA1-97AC-41BA-A22B-940C08F3C071.png 魔数(Magic Number) CAFEBABE ,表示这是一个Class类型文件,4字节。 minor version: 0(0x0000),2字节。 major version: 51 (0x0033),2字节。

6.3.2 常量池

接在魔数和版本后面的是常量池。 常量池中存放两大类型常量:

  • 字面量(Literal):文本字符串,被申明为final的常量等
  • 符号引用(Symbolic References)
    • 类和接口的全限定名,
      • #13,Lclazz/TestClass
    • 字段的名称和描述符 ,
      • #5 = Utf8               m
      • #6 = Utf8               I
    • 方法的名称和描述符,
      • #14 = Utf8             main,
      • #15 = Utf8             ([Ljava/lang/String;)V
    • 方法句柄和方法类型,
      • #23 = NameAndType        #5:#6          // m:I
    • 动态调用点和动态常量

常量池中的常量有17种类型,好比说 CONSTANT_Methodref_info,CONSTANT_Classref_info,CONSTANT_Utf8_info等等。 每种类型常量的结构也不近相同。共同点是,都已u1的tag开头,表示类型。《深刻理解Java虚拟机》中,列出了完整的定义,下面简单举例。

CONSTANT_Methodref_info:

  • tag,u1,值为10
  • index,u2指向声明方法的类描述符CONSTANT_Classref_info的索引
  • index,u2指向名称及类型描述符CONSTANT_NameAndType_info的索引

CONSTANT_Classref_info:

  • tag,u1,值为7
  • index,u2指向全限定名常量项的索引

ED0C3D04-EB72-4F88-A7DF-976C05B2CAAB.png 常量池项目数量:25 (0x001A是26,常量池索引值从1开始,0保留,因此实际只用25个常量,0能够理解成不引用常量池中的项目)。

第一项(0A 00 04 00 16), #1 = Methodref   #4.#22:

  • 0A,tag,u1 为10表示是CONSTANT_Methodref_info
  • 00 04,index,u2,所在类描述符索引,即 #4 = Class    #25      // java/lang/Object
  • 00 16,index,u2,名称及类型描述符索引,即#22 = NameAndType    #7:#8     // "":()V

值得注意的是 父类方法类型是Methodref,类方法inc类型是utf8。

第二项(09 00 03 00 17) , #2 = Fieldref           #3.#23         // clazz/TestClass.m:I

  • 09,tag,u1 为9表示是CONSTANT_Fieldref_info
  • 00 03,index,u2为 3所在类描述符索引, #3 = Class     #24       // clazz/TestClass
  • 00 17,index,u2为23名称及类型描述符索引,即 #23 = NameAndType     #5:#6      // m:I

剩余的23个项目,太多了,懂意思就行了。

6.3.3 访问标志

121C6A28-F028-418E-B254-A9DD4E548CBA.png 结束了常量池25项解析,接着是Class访问标志(access_flags) 00 21 表示 0x0020 & 0x0001

  • 0x0001 ACC_PUBLIC 是否为public类型
  • 0x0020 ACC_SUPER 是否容许使用invokespecial字节码指令的新语义,invokespecial语义在JDK1.0.2发生改变,1.0.2后编译的类,这个标志必须为真。

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

撒大声地所多.png

  • 类索引,u2,0x0003,#3 = Class              #24            // clazz/TestClass
  • 父类索引,u2,0x0004,#4 = Class          #25            // java/lang/Object
  • 接口索引数量,u2,0x0000,没有接口

6.3.5 字段表集合

image.png

  • fields_count,u2,0x0001,表示有一个字段
  • access_flags,u2,0x0002,ACC_PRIVATE
  • name_index,u2,0x0005,  #5 = Utf8      m
  • descriptor_index,u2,0x0006,  #6 = Utf8         I
  • attributes_count,u2,0x0000, 无
  • attributes_info,无

6.3.6方法表集合

image.png 方法数量,u2,0x0003,有3个方法 方法表结构

  • access_flags,u2,0x0001
  • name_index,u2,0x0007
  • descriptor_index,u2,0x0008
  • attributes_count,u2,0x0001
  • attribute_info
    • attribute_name_index,u2,0x0009,对应是Code属性,接下来按照Code来解析
    • attribute_length,u4,0x0000002f
    • max_stack,u2,0x0001,操做数栈的最大深度
    • max_locals,u2,0x0001,局部变量表所须要的空间
    • code_length,u4,0x00000005,code长度为5个u1
    • code,u1, 2A,B7,00,01,B1
      • 2A,对应指令aload_0,将第0个变量槽中reference类型的本地变量推送到操做数栈
      • B7, 对应指令invokespecial,
      • 00, 对应指令nop,什么也不作
      • 01,对应指令aconst_null,将null推到栈顶
      • B1,对应指令 return
    • exception_table_length,u2,0x0000
    • exception_table,
    • attributes_count,u2,0x0002
    • attributes。。。后面就不解析了,意会就好。

6.3.7属性表集合

在方法表中,咱们遇到了一个属性"Code",还有其余属性。

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 由final关键字定义的常量值
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 记录类文件名称
... ... ...

解析工做就不作了。

6.4 字节码指令简介

Java虚拟机指令,单字节操做码(OpCode)+操做数(Operand,零或多个)。面向操做数栈,而非寄存器。多数指令不包含操做数,指令参数放在操做数栈。 指令执行过程简易伪代码

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

6.4.1 字节码与数据类型

大多数指令都包含其操做要求的数据类型,例如iload,从局部变量表中加载int型数据到操做数栈中。 i表明int,l表明long,f表明float,a表明reference。 也有些指令跟数据类型无关,好比 goto。

6.4.2指令分类

  • 加载和存储指令, iload,iload_0,iload_1,fload,istore,bipush,sipush,wide...
  • 运算指令,iadd,isub,idiv,ishl,ior,ixor,iinc,dcmpg
  • 类型转换指令,窄化显式类型转换 i2b,i2c,d2f。
  • 对象建立与访问指令,new,newarray,anewarray,getfield,baload,iaload
  • 操做数栈管理指令,pop,pop2,dup2_x1,swap
  • 控制转移指令,ifeq,tableswitch,goto,goto_w
  • 方法调用和返回指令, invokevirtual,invokeinterface,invokespecial,invokestatic,invokedynamic
  • 异常处理指令,athrow
  • 同步指令。

其余等等。。这章就了解下字节码构成和指令。

第7章 虚拟机类加载机制

7.1 概述

类加载机制:从Class文件到内存中Java类型的过程。各个阶段时间段上能够有重叠。 类加载是在运行期间执行的,也描述为动态加载和动态链接。

7.2 类加载时机

截屏2020-08-24下午1.28.17.png 对于何时开始加载, 《Java虚拟机规范》没有强制约束。可是严格规定了有且只有六种状况,若是类没有初始化,必须当即对类进行"初始化" (加载、链接必然会先执行),称之为主动引用:

  • 遇到new、getstatic、putstatic、invokestatic字节码指令时
    • 指令new实例化对象。
    • 指令getstatic/putstatic 访问其静态对象(被final修饰,编译期已放入常量池的除外)。
    • 指令invokestatic,调用其静态方法。
  • 反射调用
  • 子类初始化时,先触发父类的初始化
  • 虚拟机启动时,初始化用户指定的要执行的主类
  • MethodHandle解析结果为REF_getStatic,REF_setStatic,REF_invokeStaitc,Ref_newInvokeSpecial
  • 当一个接口定义了JDK 8新加入的默认方法(default修饰)

不会触发类初始化的几个场景举例:

  • 经过子类引用父类的静态字段,不会致使子类初始化
  • 经过数组定义引用类,不会触发此类的初始化
  • 引用在编译期已被放入常量池的常量。
//场景一 经过子类引用父类的静态字段,不会致使子类初始化
public class SuperClass {
    static { System.out.println("SuperClass init!");}
    public static int value=123;
}

public class SubClass extends SuperClass{
    static { System.out.println("SubClass init!");}
}

public class NoInitialization1 {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

复制代码
//场景二 经过数组定义引用类,不会触发此类的初始化
public class NoInitialization2 {
    public static void main(String[] args) {
        SuperClass[] scarray=new SuperClass[10];
    }
}
复制代码
//场景三 引用在编译期已被放入常量池的常量。
public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWRLD="hello world";
}
public class NoInitialization3 {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWRLD);
    }
}
复制代码

对于场景三咱们查看NoInitialization3的字节码发现,“hello world”已经在其常量池中,使用 ldc指令将常量压入栈中。而System.out则是使用getstatic指令。这个地方不涉及到ConstClass的初始化

Constant pool:
  #4 = String             #25            // hello world
  #25 = Utf8               hello world
{
  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String hello world
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}
复制代码

7.3类加载过程

7.3.1  加载

加载(loading):从静态文件到运行时方法区。完成三件事情:

  • 经过一个类的全限定名获取定义此类的二进制字节流。
    • 能够从ZIP包中读取(JAR,WAR等等)
    • 从网络中获取,好比Web Applet
    • 运行时计算生成,好比动态代理技术, “*$Proxy”代理类
    • 数据库中读取
    • 加密文件中读取
    • ......
  • 将该字节流的类静态存储结构转化成方法区的运行时数据结构。
  • 在内存中生成一个表明这个类的java.lang.Class对象,做为方法区中这个类的各类数据的访问入口。

使用Java虚拟机内置的引导类加载器,或者用户自定义的类加载器。

7.3.2 验证

确保字节流符合《Java虚拟机规范》的约束,代码安全性问题验证。验证是重要的,但不是必须的。 四个阶段:

  • 文件格式验证,此阶段经过后,会存储到方法区。后面阶段基于方法区数据进行验证,再也不读取字节流。
  • 元数据验证,类元数据信息语义校验
  • 字节码验证,最复杂,对类的Code部分进行检验分析。程序语义合法性,安全性等等
  • 符号引用验证,在解析过程当中发生,若是没法经过符号引用验证,Java虚拟机会抛出java.lang.IncompatibleClassChangeError的子类异常,如 NoSuchFieldError,NoSuchMethodError等等。

7.3.3 准备

一般状况下,为类变量(静态变量),分配内存并设置初始值(零值)。初值并非代码中赋的值123。123要等到初始化阶段。

public static int value = 123;
复制代码

编译成class文件

public static int value;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
...
 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
         0: bipush        123
         2: putstatic     #2                  // Field value:I
         5: return     
...
复制代码

某些状况下,设置初始值为456。好比final修饰的变量。由于变量值456,会提早加入到常量池。

public static final int value2 = 456;
复制代码
public static final int value2;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 456
复制代码

7.3.4 解析

将常量池内的符号引用替换为直接引用的过程。 好比说这种,咱们要把 #2替换成实际的类引用,若是是未加载过的类引用,又会涉及到这个类加载过程。

getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
复制代码
  • 类或接口解析
  • 字段解析
  • 方法解析
  • 接口方法解析

7.3.5 初始化

执行类构造器()方法,非实例构造器()方法 。 ()方法:执行类变量赋值语句和静态语句块(static{})。顺序为其在源文件中顺序决定。 举例1:非法向前引用变量。 value的定义在 static{} 以后,只能赋值,不能读取值。

public class PrepareClass {
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
    public static int value=123;
}
复制代码

可是下面就能够

public class PrepareClass {
    public static int value=123;
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
}
复制代码

class文件参考

0: bipush        123
2: putstatic     #2                  // Field value:I
5: iconst_3
6: putstatic     #2                  // Field value:I
9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
12: getstatic     #2                  // Field value:I
15: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
18: return
复制代码

举例2: ()执行顺序。子类初始化时,要先初始化父类

public class TestCInitClass2 {

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
复制代码

输出:

2
复制代码

Java虚拟机必须保证()方法在多线程环境下的同步问题。

7.4 类加载器

实现“经过一个类的全限定名来获取其二进制字节流”的代码,称之为“类加载器”(Class Loader)。

7.4.1 类与类加载器

类与其加载器肯定了这个类在Java虚拟机中的惟一性。

三层类加载器,绝大多数Java程序会用到如下三个系统提供的类加载器进行加载:

  • 启动类加载器(BootStrap Class Loader)
  • 扩展类加载器(Extension Class Loader)
  • 应用程序类加载器(Application Class Loader)

除了以上三个还有用户自定义的加载器,经过集成java.lang.ClassLoader类来实现。

启动类加载器

加载Java的核心库,native代码实现,不继承java.lang.ClassLoader

URL[]  urls= sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
    System.out.println(url);
}

结果输出:
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/resources.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jsse.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jce.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/charsets.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jfr.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/classes
复制代码

扩展类加载器

加载Java的扩展库,加载ext目录下的Java类

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

结果输出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
复制代码

应用程序类加载器

加载Java应用的类。经过ClassLoader.getSystemClassLoader()来获取。

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

结果输出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
...
file:/.../jdk1.8.0_73.jdk/Contents/Home/lib/tools.jar
file:/.../java_sample/out/production/java_sample/  //这是咱们的应用程序
file:/Applications/IntelliJ%20IDEA.app/Contents/lib/idea_rt.jar
复制代码

自定义类加载器

7.4.2 双亲委派模型截屏2020-08-25下午5.04.25.png

ClassLoader.loadClass

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
复制代码

AppClassLoader,ExtClassLoader都继承URLClassLoader。 URLClassLoader.findClass(name)

protected Class<?> findClass(final String name)throws ClassNotFoundException {
       // 一、安全检查
    // 二、根据绝对路径把硬盘上class文件读入内存
    byte[] raw = getBytes(name); 
    // 三、将二进制数据转换成class对象
    return defineClass(raw);
    }
复制代码

若是咱们本身去实现一个类加载器,基本上就是继承ClassLoader以后重写findClass方法,且在此方法的最后调包defineClass。 ** 双亲委派确保类的全局惟一性。 例如不管哪一个类加载器须要加载java.lang.Object,都会委托给最顶端的启动类加载器加载。

参考: 通俗易懂 启动类加载器、扩展类加载器、应用类加载器 深刻探讨 Java 类加载器

7.4.3 线程上下文类加载器

线程上下文类加载器(context class loader),能够从java.lang.Thread中获取。 双亲委派模型不能解决Java应用开发中遇到的全部类加载器问题。 例如,Java提供了不少服务提供者接口(Service Provider Interface,SPI),容许第三方提供接口实现。常见的SPI有JDBC,JCE,JNDI,JAXP等。SPI接口由核心库提供,由引导类加载器加载。 而其第三方实现,由应用类加载器实现。此时SPI就找不到具体的实现了。 SPI接口代码中使用线程上下文类加载器。线程上下文类加载器默认为应用类加载器。

第8章 虚拟机字节码执行引擎

8.1 概述

虚拟机是相对于物理机的概念。 物理机的执行引擎是直接创建在处理器,缓存,指令集合操做系统底层上。 虚拟机的执行引擎是创建在软件之上,不受物理条件限制,定制指令集与执行引擎。 虚拟机实现中,执行过程能够是解释执行和编译执行,能够单独选择,或者混合使用。 但全部虚拟机引擎从统一外观(Facade)来讲,都是输入字节码二进制流,字节码解析执行,输出执行结果。

本章从概念角度讲解虚拟机的方法调用和字节码执行。

8.2 运行时栈帧结构

Java虚拟机以方法做为最基本的执行单元。每一个方法在执行时,都会有一个对应的栈帧(Stack Frame) .栈帧同时也是虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。 一个栈帧须要多大的局部变量表,须要多深的操做数栈,早在编译成字节码时就写到了方发表的Code属性中。 Code:  stack=2, locals=1, args_size=1 所以一个栈帧须要分配多少内存,在运行前就已肯定,取决于源码和虚拟机自身实现。 截屏2020-08-26上午10.42.35.png

8.2.1 局部变量表

      局部变量表容量最小单位为变量槽(Variable Slot), 《Java虚拟机规范》规定一个变量槽能够存放一个boolean,byte,char,init,float,reference或returnAddress类型的数据。32位系统能够是32位,64位系统能够是64位去实现一个变量槽。对于64位的数据类型(long和double),以高位对齐的方式分配两个连续的变量槽。 因为是线程私有,不管两个连续变量槽的读写是否为原子操做,都不会有线程安全问题。

从参数到参数列表

     当一个方法被调用时,会把参数值放到局部变量表中。类方法参数Slot从0开始。实例方法参数Slot从1开始,Slot0给了this,指向实例。 咱们比较类方法和实例方法的字节码。

public static int add(int a, int b) {return a + b;}
public int remove(int a, int b) {return a - b;}
复制代码
public static int add(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0     a   I
            0       4     1     b I public int remove(int, int);
    flags: ACC_PUBLIC
    Code:
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lexecute/Reerer;
            0       4     1     a   I
            0       4     2     b   I
复制代码

变量槽复用

当变量的做用域小于整个方法体时,变量槽能够复用,为了节约栈内存空间。好比 {},if(){}等代码块内。变量槽复用会存在“轻微反作用”,内存回收问题。

public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        System.gc();
}

//执行结果
[GC (System.gc())  69468K->66040K(251392K), 0.0007701 secs]
[Full GC (System.gc()) 66040K->65934K(251392K), 0.0040938 secs] //解释: 虽然placeholder的做用域被限制,但gc时,局部变量表仍然引用placeholder,没法被回收。 复制代码
public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        int i=0;
        System.gc();
}
//执行结果
[GC (System.gc())  69468K->66040K(251392K), 0.0007556 secs]
[Full GC (System.gc()) 66040K->398K(251392K), 0.0044978 secs] //解释: 虽然placeholder的做用域被限制,int i=0复用了slot0,切断了局部变量表的引用placeholder。 复制代码
public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
            placeholder=null;
        }
        System.gc();
}
//执行结果
[GC (System.gc())  69468K->66088K(251392K), 0.0022762 secs]
[Full GC (System.gc()) 66088K->398K(251392K), 0.0050265 secs] //解释 主动释放placeholder 复制代码

局部变量赋值

类变量在准备阶段,会被赋默认零值。而局部变量没有准备阶段。因此下面代码是编译不经过的,即使编译经过,在检验阶段,也会被发现,致使类加载失败。

public static void fun4(){
     int a;
     
     //编译失败,Variable ‘a’ might not have been initialized
     System.out.println(a);
}
复制代码

8.2.2操做数栈

操做数栈(Operand Stack)      字节码指令读取和写入操做数栈。操做数栈中元素的数据类型必须与指令序列严格匹配。编译阶段和类检验阶段都会去保证这个。     在大多数虚拟机实现中,上面栈帧的操做数栈与下面栈帧的局部变量会有一部分重叠,这样不只节约了空间,重要的是在方法调用时直接公用数据,无须而外的参数复制。

8.2.3 动态链接

在类加载过程当中,会把符号引用解析为直接引用。方法调用指令以常量池中的符号引用为参数。这些方法符号引用一部分在类加载或者第一次使用时转化为直接引用,这种转化被称为静态解析。另一部分则须要在每次运行期间转化为直接引用,这部分称之为动态链接。

8.2.4 方法返回地址

正常调用完成和异常调用完成。 恢复主调方法的执行状态。

8.3 方法调用

Java虚拟机中的5条方法调用指令:

  • invokestatic,调用静态方法。
  • invokespecial,调用实例构造器()方法,私有方法和父类中的方法。
  • invokevirtual,调用虚方法?
  • invokeinterface,调用接口方法,会在运行期,肯定一个该接口的实现对象。
  • invokedynamic,运行时动态解析出调用点限定符所引用的方法,而后再执行该方法。前4个指令逻辑固化在虚拟机内部,而invokedynamic指令的分派逻辑由用户设定的引导方法决定。

方法按照类加载阶段是否能转化成直接引用分类,能够分为:

  • 非虚方法(Non-Virtual Method), 类加载阶段,直接把符号引用解析为该方法的直接引用。
    • 包括能够被invokestatic调用的静态方法,
    • 包括能够被invokespecial调用的实例构造器,私有方法,父类方法
    • final修饰的方法(尽管它使用invokevirtual调用),此类型方法没法被覆盖,不存在多太选择,是惟一的。
  • 虚方法 (Virtual Method) ,其余方法。

8.3.1 解析

非虚方法的调用称之为解析(Resolution),"编译器可知,运行期不可变",即类加载阶段把符号引用转化为直接引用。 而另一个方法调用的方式称之为分派(Dispatch)。

8.3.2 分派

分派(Dispatch)是静态或者动态的,又或者是单分派或者多分派。重载或者重写会出现同名方法。同名方法的选择,我能够称之为分派

1. 静态分派

Method Overload Resolution, 这部份内容实际上叫作方法重载解析。静态分派发生在编译阶段。 先来看一段代码,sayHello方法重载。

//方法静态分派
public class StaticDispatch {

    static abstract  class Human{}

    static class Man extends Human{}

    static class Woman extends Human{}

    public static void sayHello(Man man){System.out.println("hello,gentleman!"); }

    public static void sayHello(Human guy){ System.out.println("hello,guy!");}

    public static void sayHello(Woman women){System.out.println("hello,lady!");}

    public static void main(String[] args) {
        Human man=new Man();
        Human woman=new Woman();
        StaticDispatch dispatch=new StaticDispatch();
        dispatch.sayHello(man);
        dispatch.sayHello(woman);
    }
}

//执行结果:
hello,guy!
hello,guy!
复制代码

对应Class字节码

public static void main(java.lang.String[]);
    Code:
      stack=2, locals=3, args_size=1
         0: new           #7                  // class execute/StaticDispatch$Man
         3: dup
         4: invokespecial #8                  // Method execute/StaticDispatch$Man."<init>":()V
         7: astore_1
         8: new           #9                  // class execute/StaticDispatch$Woman
        11: dup
        12: invokespecial #10                 // Method execute/StaticDispatch$Woman."<init>":()V
        15: astore_2
        16: new           #11                 // class execute/StaticDispatch
        19: dup
        20: invokespecial #12                 // Method "<init>":()V
        23: astore_3
        24: aload_3
        25: aload_1
        26: invokevirtual #13                 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
        29: aload_3
        30: aload_2
        31: invokevirtual #13                 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
        34: return
复制代码

第0~15行,咱们构建了Man对象和Woman对象,并放入了局部变量表中。 第26行,执行方法Method sayHello:(Lexecute/StaticDispatch H u m a n ; ) V ,第 31 行,执行方法 M e t h o d s a y H e l l o : ( L e x e c u t e / S t a t i c D i s p a t c h Human;)V, 第31行,执行方法Method sayHello:(Lexecute/StaticDispatch Human;)V, 实际执行的都是sayHello(Human)。而不是sayHello(Man)或者sayHello(Woman)。 这里涉及到两个类型:

  • 静态类型(Static Type),或者叫“外观类型(Apparent Type)”,即Human
  • 实际类型(Actual Type),或者叫“运行时类型(Runtime Type)”,即Man,Woman

编译期并不知道对象的实际类型,因此按照对象的静态类型去分派方法。

2. 动态分派

与重写(Override)密切关联。动态分派发生在运行期间。在运行时,肯定方法的接收者(方法所属对象)

//方法动态分派
public class DynamicDispatch {

    static abstract class Human {
        public void sayHello() {System.out.println("hello,guy!");}
    }

    static class Man extends Human {
        public void sayHello() {System.out.println("hello,gentleman!"); }
    }

    static class Woman extends Human {
        public void sayHello() {System.out.println("hello,lady!");}
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();//hello,gentleman!
        woman.sayHello();//hello,lady!
        man = new Woman();
        man.sayHello();//hello,lady!
    }
}

//执行结果:
hello,gentleman!
hello,lady!
hello,lady!
复制代码

对应字节码

0: new   #2  // class execute/DynamicDispatch$Man
3: dup
4: invokespecial #3  // Method execute/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new   #4  // class execute/DynamicDispatch$Woman
11: dup
12: invokespecial #5  // Method execute/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6  // Method execute/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6  // Method execute/DynamicDispatch$Human.sayHello:()V
24: new   #4  // class execute/DynamicDispatch$Woman
27: dup
28: invokespecial #5  // Method execute/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6  // Method execute/DynamicDispatch$Human.sayHello:()V
36: return
复制代码

第7行,astore_1存储了Man对象 第15行,astore_2存储了Woman对象 第16,17行,aload_1,invokevirtual.实际调用的是Man.sayHello()方法 第20,21行,aload_2,invokevirtual.实际调用的是Woman.sayHello()方法 第31行,astore_1存储了Woman对象 第32,33行,aload_1,invokevirtual.实际调用的是Woman.sayHello()方法

运行期间,选择是根据man和woman对象的实际类型分派方法。

小知识:字段永远不参与多态,方法中访问的属性名始终是当前类的属性。子类会遮蔽父类的同名字段

3.单分派与多分派

方法的宗量:方法的接收者与方法的参数 单分派:基于一种宗量分派 多分派:基于多种宗量分派。 当前Java语言是一门静态多分派,动态单分派的语言。编译期根据方法接收者和参数肯定方法的符号引用。运行期根据方法的接收者,解析和执行符号引用。

考虑下面一段代码

public class Dispatch {

    static class Father{
        public void f() {System.out.println("father f void");}
        public void f(int value) {System.out.println("father f int");}
    }
    static  class Son extends Father{
        public void f(int value) {System.out.println("Son f int"); }
        public void f(char value) { System.out.println("Son f char");}
    }

    public static void main(String[] args) {
        Father son=new Son();
        son.f('a');
    }
}
//执行结果: Son f int
复制代码

字节码

0: new           #2                  // class execute/Dispatch$Son
3: dup
4: invokespecial #3                  // Method execute/Dispatch$Son."<init>":()V
7: astore_1
8: aload_1
9: bipush        97
11: invokevirtual #4                  // Method execute/Dispatch$Father.f:(I)V
复制代码

首先是编译期的静态分派,先选择静态类型Father,因为Father中没有f(char),则会选择最合适的f(int),肯定方法为Father.f:(I)V。其次是运行期,接收者为Son,Son中有重写的f:(I)V。因此最终执行的是Son.f:(I)V

4. 虚拟机动态分派的实现

虚方法表,接口方法表,类型继承分析,守护内联,内联缓存 截屏2020-08-27上午11.41.15.png

8.4 动态类型语言支持

8.4.1 动态类型语言

动态类型语言的关键特征:类型检查的主体过程是在运行期而不是编译器,好比说Groovy、JavaScript、Lisp、Lua、Python。 静态类型语言:编译器就进行类型检查,好比C++,Java。

8.4.2 Java与动态类型

Java虚拟机须要支持动态类型语言,因而在JDK7发布 invokedynamic指令。

8.4.3 java.lang.invoke包

8.4.4 invokedynamic指令

8.5  基于栈的字节码解释执行引擎

8.5.1 解释执行

8.5.2 基于栈的指令集合基于寄存器的指令集

8.5.3 基于栈的解释器执行过程

public int calc() {
    int a = 100;
    int b = 200;
    int c = 300;
    return (a + b) * c;
}
复制代码
stack=2, locals=4, args_size=1
         0: bipush        100   //常量100压入操做数栈顶
         2: istore_1		    //栈顶元素(100)存入变量槽1,同时消费掉栈顶元素
         3: sipush        200   //常量200压入操做数栈顶
         6: istore_2            //栈顶元素(200)存入变量槽2,同时消费掉栈顶元素
         7: sipush        300   //常量300压入操做数栈顶
        10: istore_3            //栈顶元素(300)存入变量槽3,同时消费掉栈顶元素
        11: iload_1             //将局部变量slot1值100压入操做数栈顶
        12: iload_2             //将局部变量slot2值200压入操做数栈顶
        13: iadd                //消费栈顶100和200,获得300,并压入栈顶
        14: iload_3				//将局部变量slot3值300压入操做数栈顶
        15: imul				//消费栈顶300和300,获得90000,并压入栈顶
        16: ireturn             //消费栈顶90000,整型结果返回给方法调用者
复制代码

第10章 前端编译与优化

10.1 概述

先明确几个概念 即时编译器(JIT编译器,Just In Time Compiler),运行期把字节码变成本地代码的过程。 提早编译器(AOT编译器,Ahead Of Time Compiler),直接把程序编译成与目标及其指令集相关的二进制代码的过程。

这里讨论的“前端编译器”,是指把*.java文件转换成*.class文件的过程,主要指的是javac编译器。

10.2 Javac编译器

10.2.1 介绍

Javac编译器是由Java语言编写。分析Javac代码的整体结构来看,编译过程大体分为1个准备过程和3个处理过程。以下:

  • 1)准备过程:初始化插入式注解处理器
  • 2)解析与填充符号表
    • 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
    • 填充符号表。产生符号地址和符号信息。
  • 3)插入式注解处理器的注解处理
  • 4)分析与字节码生成
    • 标注检查。对语法的静态信息进行检查。
    • 数据流及控制流分析。对程序动态运行过程进行检查
    • 解语法糖。将语法糖代码还原为原有形式
    • 字节码生成。将前面各个步骤生成的信息转化为字节码。

若是注解处理产生新的符号,又会再次进行解析填充过程。 截屏2020-08-28上午10.10.16.png Javac编译动做的入口com.sun.tools.javac.main.JavaCompiler类。代码逻辑主要所在方法compile(),compile2() 截屏2020-08-28上午11.24.33.png

10.2.2 解析和填充符号表

1. 词法、语法分析

对应parserFiles()方法 词法分析:源码字符流转变为标记(Token)集合的过程。标记是编译时的最小元素。关键字、变量名、字面量、运算符都是能够做为标记。 如“int a = b + 2”, 包含了6个标记,int,a , =,  b,  +, 2 。词法分析由com.sun.tools.javac.parser.Scanner实现。 语法分析:根据标记序列构造抽象语法树的过程。抽象语法树(Abstract Syntax Tree,AST),描述代码语法结构的树形表示形式,树的每一个节点都表明一个语法结构,例如包,类型,运算符,接口,返回值等等。com.sun.tools.javac.parser.Parser实现。抽象语法树是以com.sun.tools.javac.tree.JCTree类表示。 后续的操做创建在抽象语法树之上。

2.填充符号表

对应enterTree()方法。

10.2.3 注解处理器

      JDK6,JSR-269提案,“插入式注解处理器”API。提早至编译期对特定注解进行处理,能够理解成编译器插件,容许读取、修改、添加抽象语法树中的任意元素。若是产生改动,编译器将回到解析及填充符号表过程从新处理,直到不产生改动。每一次循环过程称为一个轮次(Round).    使用注解处理器能够作不少事情,譬如Lombok,能够经过注解自动生成getter/setter方法、空检查、产生equals()和hashCode()方法。

10.2.4 语义分析与字节码生成

抽象语法树可以表示一个正确的源程序,但没法保证语义符合逻辑。语义分析的主要任务是进行类型检查、控制流检查、数据流检查等等。 例如

int a = 1;
boolean b = false;
char c = 2;

//后续可能出现的运算,都是能生成抽象语法树的,但只有第一条,能经过语义分析
int  d= a + c;
int  d= b + c;
char d= a + c;
复制代码

在IDE中看到的红线标注的错误提示,绝大部分来源于语义分析阶段的结果。

1. 标注检查

attribute()方法,检查变量使用前是否已被声明,变量与赋值的数据类型是否匹配等等。 3个变量的定义属于标注检查。标注检查顺便会进行极少许的一些优化,好比常量折叠(Constant Folding).

int a = 1 + 2; 实际会被折叠成字面量“3复制代码

2. 数据及控制流分析

flow()方法,上下文逻辑进一步验证,好比方法每条路径是否有返回值,数值操做类型是否合理等等。

3. 解语法糖

语法糖(Syntactic Sugar),编程术语 Peter J.Landin。减小代码量,增长程序可读性。好比Java语言中的泛型(其余语言的泛型不必定是语法糖实现,好比C#泛型直接有CLR支持),变长数组,自动装箱拆箱等等。 解语法糖,编译期将糖语法转换成原始的基础语法。

4. 字节码生成

  • 将前面生成的信息(语法树,符号表)转化为字节码,
  • 少许代码添加,(),()等等
  • 少许代码优化转换,字符串拼接操做替换为StringBuffer或StringBuilder等等。

10.3 Java语法糖

10.3.1 泛型

1.Java泛型

       JDK5,Java的泛型实现称为“类型擦除式泛型”(Type Erasure Generic),相对的C#选择的是“具现化泛型”(Reified Generics),C#泛型不管在源码中,仍是编译后的中间语言表示(此时泛型都是一个占位符),List 与List是两个不一样的类型。而Java泛型,只是在源码中存在,编译后都变成了统一的类型,称之为类型擦除,在使用处会增长一个强制类型转换的指令。

Map<String, String> stringMap = new HashMap<String, String>();
stringMap.put("hello", "你好");
System.out.println(stringMap.get("hello"));

Map objeMap = new HashMap();
objeMap.put("hello2", "你好2");
System.out.println((String)objeMap.get("hello2"));
复制代码

截取部分字节码

0: new           #2                  // class java/util/HashMap
4: invokespecial #3                  // Method java/util/HashMap."<init>":()V
13: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
25: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
30: checkcast     #9                  // class java/lang/String
33: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
        
36: new           #2                  // class java/util/HashMap
40: invokespecial #3                  // Method java/util/HashMap."<init>":()V
49: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
61: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
66: checkcast     #9                  // class java/lang/String
69: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

72: return
复制代码

能够看到两部分代码在编译后是同样的。 第0行,new HashMap<String, String>() 实际构造的是java/util/HashMap。 第30行,stringMap.get("hello") ,checkcast指令,作了一个类型转换

2. 历史背景

2004年,Java5.0。为了保证代码的“二进制向后兼容”,引入泛型后,原先的代码必须可以编译和运行。例如Java数组支持协变,集合类也能够存入不一样类型元素。 代码以下

Object[] array = new String[10]; 
array[0] = 10; // 编译期不会有问题,运行时会报错 

ArrayList things = new ArrayList(); 
things.add(Integer.valueOf(10)); //编译、运行时都不会报错
things.add("hello world");
复制代码

若是要保证Java5.0引入泛型后,上述代码依然能够运行,有两个选择:

  • 原先须要泛型化的类型保持不变,再新增一套泛型化的类型版本。泛型具现化,好比C#新增了一组System.Collections.Generic的新容器,原先的System.Collections保持不变。
  • 把须要泛型化的类型原地泛型化,Java5.0采用的原地泛型化方式为类型擦除。

为什么C#与Java的选择不一样,主要是C#当时才2年遗留老代码少,Java快10年了老代码多。类型擦除是偷懒留下的技术债。

3.类型擦除

类型擦除除了前面提到的编译后都变成了统一的裸类型以及使用时的类型检查和转换以外还有其余缺陷。 1)不支持原始类型(Primitive Type)数据泛型,ArrayList须要使用其对应引用类型ArrayList,致使了读写的装箱拆箱。 2)运行期没法获取泛型类型信息,例如

public  <E> void doSomething(E item){
        E[] array=new E[10];  //不合法,没法使用泛型建立数组
        if(item instanceof  E){}//不合法,没法对泛型进行实例判断
}
复制代码

当咱们去写一个List到数组的转换方法时,须要额外传递一个数组的组件类型

public  static <T> T[] convert(List<T> list,Class<T> componentType){
        T[] array= (T[]) Array.newInstance(componentType,list.size());
        for (int i = 0; i < list.size(); i++) {
            array[i]=list.get(i);
        }
        return array;
}
复制代码

3)类型转换问题。

//没法编译经过
//虽然String是Object的子类,但ArrayList<String>并非ArrayList<Object>的子类。
ArrayList<Object> list=new ArrayList<String>();
复制代码

为了支持协变和逆变,泛型引入了 extends ,super

//协变 
ArrayList<? extends Object> list = new ArrayList<String>();

//逆变
ArrayList<? super String> list2 = new ArrayList<Object>();
复制代码

4 值类型与将来泛型

2014年,Oracle,Valhalla语言改进项目内容之一,新泛型实现方案

10.3.2 其余

自动装箱,自动拆箱,遍历循环,变长参数,条件编译,内部类,枚举类,数值字面量,switch,try等等。

10.3.3 *扩展阅读

Java协变介绍 Lambda与invokedynamic

10.4 实战 Lombok注解处理器

第11章 后端编译与优化

11.1概述

前面一章讲的是从*.java到*.class的过程,即源码到字节码的过程。 这一章讲的是从二进制字节码到目标机器码的过程,分为两种即时编译器和提早编译器。

11.2 即时编译器

目前主流的两款商用Java虚拟机(HotSpot、OpenJ9)里,Java程序最初都是经过解释器 (Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认 定为“热点代码”(Hot Spot Code),为了提升热点代码的执行效率,在运行时,虚拟机将会把这些代 码编译成本地机器码,并以各类手段尽量地进行代码优化,运行时完成这个任务的后端编译器被称 为即时编译器。

11.3 提早编译器

  • 即时编译消耗的时间都是本来可用于程序运行的时间,消耗的运算资源都是本来可用

于程序运行的资源,

  • 给即时编译器作缓存加速,去改善Java程序的启动时间,以

及须要一段时间预热后才能到达最高性能的问题。这种提早编译被称为动态提早编译(Dynamic AOT)或者索性就大大方方地直接叫即时编译缓存(JIT Caching)

Android虚拟机历程: Android4.4以前 Dalvik虚拟机 即便编译器 Android4.4开始 Art虚拟机 提早编译器,致使安装时须要编译App,很是耗时,但运行性能获得提高 Android7.0开始 从新启用解释执行和即时编译,系统空闲时间时自动进行提早编译。

11.3 编译器优化技术

第12章 Java内存模型与线程

12.1 概述

介绍虚拟机如何实现多线程,多线程之间因为共享数据而致使的一系列问题及解决方案

12.2 硬件效率与一致性

介绍Java虚拟机内存模型前,先了解下物理机的并发问题。

  • 硬件效率问题。计算机处理任务除了处理器计算外,还有内存交互,即读写数据。而存储设备与处理机运行速度相差几个数量级,为此引入了读写速度尽量接近处理器的高速缓存Cache。处理器读写缓存数据,缓存将数据同步到内存。
  • 缓存一致性问题。在共享内存多核系统中,每一个处理器都有本身的高速缓存,又共享同一主内存。为了解决一致性问题,处理器访问高速缓存时,须要遵循一些协议,好比MSI,MESI,MISI,Synapse,Dragon Protocol等。
  • 代码乱序执行优化问题。处理器为了提升运算效率,会出现不按顺序执行的状况,但单线程下,处理器会保证执行结果与顺序执行结果一致。而多线程的状况下,没法保证多个任务都按照顺序执行。

Java虚拟机有本身的内存模型,也会有与物理机类型的问题。

截屏2020-09-01上午9.38.11.png

12.3 Java内存模型

12.3.1 概述

     Java内存模型规定:因此变量都存储在主内存(Main Memory)中,线程有本身的工做内存,工做内存保存变量在主内存副本。线程对变量的读写只能再工做内存(Working Memory)中,线程间共享变量须要经过主内存完成。      JVM内存模型的执行处理将围绕解决两个问题展开:

  • 工做内存数据一致性
  • 指令重排序优化,编译期重排序和运行期重排序。

截屏2020-09-01上午9.37.19.png

12.3.2 内存交互操做

主内存与工做内存的交互协议定义以下操做,Java虚拟机必须保证这些操做是原子性的。

  • lock,做用于主内存变量,把变量标识为线程独占状态,使其余线程没法lock
  • unlock,做用于主内存变量,解除线程独占状态
  • read,做用于主内存变量,把变量值传输到工做内存中,一边随后的load使用
  • load,做用于工做内存变量,把read的变量值放入工做内存的变量副本中。
  • use,做用于工做内存变量,变量值传递给执行引擎
  • assign,做用于工做内存变量,执行引擎赋值给工做内存中的变量
  • store,做用于工做内存变量,变量值传输到主内存,以便后续write使用
  • write,做用于主内存变量,把store的变量值放入主内存变量中。

截屏2020-09-01下午1.31.23.png 若是要把变量从主内存拷贝到工做内存,必须顺序执行 read和load,但不要求必定连续。 若是要把变量从工做内存同步到主内存,必须顺序执行 store和write,但不要求必定连续。

12.3.3 内存模型运行规则

1.内存交互基本操做的3个特性

Java内存模型是围绕着在并发过程当中如何处理这3个特性来创建的,归根结底是为了实现共享变量在多个工做内存中的一致性,以及并发时,程序能如期运行。

  • 原子性(Atomicity),即一个操做或者多个操做,要么不执行,要么所有执行且执行过程不会被打断
  • 可见性(Visibility),当多个线程访问同一个变量时,一个线程改变了变量值,其余线程要能当即看到修改过的值。线程经过共享主内存实现可见性。
  • 有序性(Ordering),线程内指令串行(as-if-serial),线程间,对于同步(synchrinized)代码以及volatile字段的操做须要维持相对有序

2.先行发生原则

happens-before

  • 程序次序规则
  • 管程锁定规则
  • volatile变量规则
  • 线程启动规则
  • 线程终止规则
  • 线程中断规则
  • 对象终结规则
  • 传递性

3.内存屏障

内存屏障是被插入到两个CPU指令之间的一种指令,用来禁止处理器指令发生指令重排序。

12.3.4 volatile型变量

volatile主要有下面两种语义

语义1 保证可见性

保证了不一样线程对该volatile型变量操做的内存可见性,但不等同于并发操做的安全性

  • 线程写volatile变量的过程assign-store-write必须连续出现:
    • 改变工做内存中volatile变量副本的值
    • 将改变的副本值刷新到主内存中
  • 线程读volatile变量的过程read-load-use必须连续出现:
    • 从主内存读取volatile变量值并存入工做线程副本
    • 从工做内存读取变量副本

语义2 禁止指令重排序截屏2020-09-01下午2.22.56.png

volatile型变量使用场景总结起来就是"一次写入,处处读取",某个线程负责更新变量,其余线程只读取变量,并根据变量新值执行相应逻辑,例如状态标志位更新,观察者模型变量值发布

相关文章
相关标签/搜索