JVM_2

虚拟机类加载机制

虚拟机类加载过程:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可用被虚拟机直接使用的Java类型

类的生命周期

解析阶段在某些情况可以在初始化阶段之后开始,为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)

这些阶段通常是互相交叉地混合进行,只不过会在一个阶段执行地过程中调用、激活另一个阶段

主动引用:必须立即对类进行“初始化”的有且仅有六种情况(加载、验证、准备自然需要在此之前开始)

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行初始化过,则需要先触发初始化阶段。生成这四个关键字的场景有:
    • 使用new关键字实例化对象的时候
    • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
    • 调用一个类型的静态方法的时候
  2. 调用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的哪个类),虚拟机会先初始化这个类
  5. 当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  6. 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

被动引用

  • 通过子类引用父类的静态字段,不会导致子类初始化
    package chapter7;
    
    /**
     * VM args:-XX:+TraceClassLoading 
     * 通过子类引用父类的静态字段,子类不会初始化
     */
    
    public class NotInitialization1{
      public static void main(String[] args) {
          System.out.println(Subclass.value);
      }
    }
    
    class SuperClass {
      static {
          System.out.println("SuperClass init!");
      }
      public static int value = 123;
    }
    class Subclass extends SuperClass{
      static {
          System.out.println("SubClass init!");
      }
     }
    
    

子类加载但没有初始化

  • 通过数组定义来引用类,不会触发此类的初始化

    package chapter7;
    
    /**
     *通过数组定义来引用类,不会触发此类的初始化
     */
    
    public class NotInitialization2{
      public static void main(String[] args) {
          SuperClass[] sca = new SuperClass[10];
      }
    }
    
    

    没有输出“SuperClass init!”

  • 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

    package chapter7;
    
    public class NotInitiallization3 {
      public static void main(String[] args) {
          System.out.println(ConstClass.HELLOWORLD);
      }
    }
    
    class ConstClass{
     static {
         System.out.println("ConstClass init!");
     }
     public static final String HELLOWORLD = "hello world";
    }
    
    

    没有输出“ConstClass init!”,其实在编译阶段通过常量传播优化,已经将此常量的值直接存储在NotInitiallization3的常量池,因此ConstClass.HELLOWORLD实际上是对自身常量池的引用。

接口的初始化与类的初始化有所不同(有且仅有情况第三条),一个接口初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化

类加载过程

加载

加载阶段要完成的三件事情

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

非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性

加载阶段和连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,这两个阶段的开始时间仍然保持着固定的先后顺序

验证

确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

验证四阶段:

  1. 文件格式验证
    验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理

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

    该验证阶段是基于二进制字节流进行的,的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求

    通过此阶段后这段字节流进入方法区存储,后面的三个阶段都是基于方法去的存储结构进行,不会再读取、操作字节流

  2. 元数据验证
    对字节码描述的信息进行语义分析,保证其描述的信息符合《Java语言规范》的要求

    • 这个类是否有父类(除了java.lang.Object外,所有的类都应当有父类)
    • 这个类是否继承了不允许被继承的类(被final修饰的类)
    • 如果这个类不是抽象类,是否实现类其父类或接口之中要求实现的所有方法
    • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
    • .......

    主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息

  3. 字节码验证
    对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为

    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况
    • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
    • 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的
    • .......

    主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的

  4. 符号引用验证
    发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段(解析)发生。检查该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源

    • 符号引用中通过字符串描述的全限定名是否能找到对应的类
    • 在指定类中是否存在符合方法的字段描述及简单名称所描述的方法和字段
    • 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问
    • ........

    主要目的是确保解析行为能正常执行,如果不通过,则抛出一个java.lang.IncompatibleClassChangeError(java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等)

    如果程序运行的全部代码都已经被反复使用和验证过,在生产环境的实施阶段可以考虑-Xverify:none参数来关闭大部分的类的验证措施

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中

public static int value = 123;
变量value在准备阶段过后的初始值为0而不是123,因为这时还未执行任何Java方法,而把value赋值为123的putstatic指令时程序被编译后,存放于类构造器<clinit>()方法中,所以这个赋值动作是要到类的初始化阶段才会完成

基本数据类型的零值

数据类型 零值 数据类型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char '\u0000' reference null
byte (byte)0

如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,public static final int value = 123;,在准备阶段value的值就为123

解析

Java虚拟机将常量池内的符号引用替换为直接引用的过程

  • 符号引用
    符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可,与虚拟机内存布局无关,引用的目标不一定已经加载到虚拟机内存
  • 直接引用
    直接引用时可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄,与虚拟机内存布局相关,引用的目标必定已经在虚拟机内存存在

对方法或者字段的访问,也会在解析阶段中对它们的可访问性(private、protected、public、<package>)进行检查

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行

初始化

初始化阶段,会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。

初始化阶段就是执行类构造器<clinit>()方法的过程,<clinit>()是Javac编译器的自动生成物

  • <clinit>()方法是由编译器自动收集类中的所有变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集顺序是由语句在源文件中出现的顺序决定的,静态语句块中之内访问到定义在静态语句块之前的变量,定义在它之后的变量,静态语句块可以赋值,但是不能访问
    public class Test {
        static {
            i = 0;  // 给变量赋值可以正常编译通过
            System.out.print(i);  // 这句编译器会提示“非法前向引用”
        }
        static int i = 1;
    }
    
  • <clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕,java.lang.Object的<clinit>()肯定是第一个执行的
  • <clinit>()方法对于类和接口来说不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不生产这个类的<clinit>()方法
  • 接口中不能使用静态语句块,但仍然有变量初始化赋值的操作,所以接口也会生成<clinit>()方法。与类不同的是,执行接口的<clinit>()方法不用先执行父接口的<clinit>()方法,除非父接口中定义的变量被使用,以及接口的实现类初始化时也不会执行接口的<clinit>()方法
  • Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程去初始化这个类,那么只有其中一个线程去执行该类的<clinit>()方法,其他线程需要阻塞等待。如果一个类的<clinit>()方法中有耗时很长的操作,可能导致多个进程堵塞

类加载的过程中,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机主导,直到初始化阶段,虚拟机才真正开始执行程序代码,主导权移交给应用程序。

类加载器

用于实现类的加载动作。对于任意一个类,都要有加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。每个类加载器都拥有一个独立的类名称空间

比较两个类是否相等,看这两个类是否由同一个类加载器加载。即使这两个类来源于同Class文件,但被不同加载器加载,那这两个类必定不相等

双亲委派模型

两种不同的类加载器

  • 启动类加载器(Bootstrap ClassLoader),使用C++语言实现,是虚拟机自身的一部分
  • 其他所有的类加载器,都由Java语言实现,独立于虚拟机外部,继承自抽象类java.lang.ClassLoader

三层类加载器

  • 启动类加载器(Bootstrap Class Loader)
    负责加载存放在<JAVA_HOME>\lib目录或者被-Xbootclasspath参数所指定的路径中存放的,而且Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的即使放在lib目录下也不会被加载)类库加载到虚拟机内存中

    无法被Java程序直接引用,如果需要把加载请求委派启动类加载器去处理,直接使用null代替即可

  • 扩展类加载器(Extension Class Loader)
    负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库

    允许用户将具有通用性的类库放置在ext目录扩展Java SE功能

  • 应用程序类加载器(Application Class Loader
    它是ClassLoader类中的getSystemClassLoader()方法的返回值,又叫“系统类加载器”

    负责加载用户类路径(ClassPath)上的所有类库,程序的默认类加载器


    双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般不是继承关系,而是通常使用组合关系来复用父加载器的代码

工作过程
如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器完成,每一个层次的类加载器都如此。因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

好处:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。比如类java.lang.Object,它处于rt.jar中,无论哪一个类加载器请求加载这个类,都会委派给处于模型最顶端的启动类加载器加载,所以在各种类加载器环境中能保证Object类是同一个类

破坏双亲委派模型

双亲委派模型并不是一个具有强制性约束的模型

只要有明确的目的和充分的理由,突破就有原则无疑是一种创新,比如OSGi中的类加载器

JDK9之后的类加载器委派模型
JDK9之后的类加载器委派模型
  • 启动类加载器负责加载的模块


  • 平台类加载器负责加载的模块


  • 应用程序类加载器负责加载的模块


虚拟机字节码执行引擎

“虚拟机”:执行引擎由软件自行实现,不受物理条件制约
“物理机”:执行引擎直接建立在处理器、缓存、指令集和操作系统层面上

所有的虚拟机输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果

运行时栈结构

Java虚拟机以方法作为最基本的执行单位,“栈帧”则是用于支持虚拟机进行方法调用和方法执行背后的数据结构

每一个栈帧都包括了局部变量被、操作数栈、动态连接、方法返回地址和一些额外的附加信息

只有位于栈顶的方法才是在运行的,称为“当前栈帧”,与之关联的方法称为“当前方法”,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作


栈帧的概念结构
局部变量表

是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量

在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量

局部变量表的容量以变量槽为最小单位,一个变量槽可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference、returnAddress);对于64位的数据类型,Java虚拟机会以高位对其的方式为其分配两个连续的变量槽空间(long、double)

Java虚拟机通过索引定位的方式使用局部变量表,索引值范围从0到局部变量表最大的变量槽数量。访问32位数据类型的变量,则索引N代表使用了第N个变量槽;访问64位数据类型的变量,则会同时使用第N和第N+1两个变量槽

当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰的),那局部变量表的第0位索引的变量槽是用于传递方法所属对象实例的引用,可以通过“this”来访问这个隐含的参数

为了节省栈帧耗用的内存空间,变量槽是可以重用的

局部变量不像内部变量那样存在“准备阶段”,如果一个局部变量定义了但没有赋初值,那它是完全不能使用的

操作数栈

也被称为操作栈,被编译成Class文件时,就在方法的Code属性的max_stacks数据项中确定了最大深度

32位数据类型占栈容量为1,64位数据类型占栈容量为2

当一个方法刚开始执行时,操作数栈是空的。执行过程中,由各种字节码指令往操作数栈中压入和提取内容

两个不同栈帧在概念模型中是完全相互独立的,但是大多数虚拟机的实现里会进行优化处理,让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,不仅节约了空间,进行方法调用的时候可以直接共用一部分数据

Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,其中的“栈”为操作数栈

两个栈帧之间的数据共享
动态连接

每个栈帧都包含一个指向常量池中该栈帧所属方法的引用,持有这个引用时为了支持方法调用过程中的动态连接

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数

静态解析:符号引用在类加载阶段或第一次使用的时候就被转化为直接引用
动态连接:符号引用在每一次运行期间都转化为直接引用

方法返回地址

当方法执行时,两种方式退出方法

  • “正常调用完成”
    执行引擎遇到任意一个方法返回的字节码指令,可能会有返回值传递给上层的方法调用者(调用当前方法的方法被称为调用者或主调方法)
  • “异常调用完成”
    在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的

无论采用何种退出方式,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态

附加信息

《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,比如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现

方法调用

方法调用阶段的唯一任务就是确定被调用的方法版本(即调用哪个方法)

解析

符合“编译期可知,运行期不可变”这个要求的方法的调用被称为解析

主要有静态方法和私有方法两大类,这两种方不可能通过继承或别的方式重写其他版本

解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用

调用不同的方法,字节码指令设计不同的指令

  • invokestatic:用于调用静态方法
  • invokespecial:用于调用实例构造器<init>()方法、私有方法、父类中的方法
  • invokevirtual:用于调用所有的虚方法
  • invokeinterface:用于调用接口方法
  • invokedynamic:先在执行时动态解析出调用点限定符所引用的方法,然后再执行该方法,前面4条指令,分派逻辑都固化再Java虚拟机内部,而这个指令的分派逻辑由用户设定的引导方法来决定

虚方法和非虚方法

  • “非虚方法”
    静态方法、私有方法、实例构造器、父类方法、被final修饰的方法(尽管它使用invokevirtual调用)
  • “虚方法”
    其他方法
分派
  • 静态分派

    package chapter8;
    
    /**
     * Human man = new Man()
     * Human:变量的“静态类型”
     * Man:变量的“实际类型”
     * 虚拟机在常在时是通过参数的静态类型而不是实例类型作为判定依据
     */
    public class StaticDispatch {
      static abstract class Human{
    
        }
    
        static class Man extends Human{
    
        }
        static class Woman extends Human{
    
      }
    
      public void sayHello(Human gay){
          System.out.println("hello,guy!");
      }
    
      public void sayHello(Man guy){
          System.out.println("hello,gentleman!");
      }
    
      public void sayHello(Woman guy){
          System.out.println("hello,lady!");
      }
    
      public static void main(String[] args) {
          Human man = new Man();
          Human woman = new Woman();
          StaticDispatch sr = new StaticDispatch();
          sr.sayHello(man);   // 输出hello,guy
          sr.sayHello(woman); // 输出hello,guy
      }
    }
    
    

    所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载

     package chapter8;
    /**
     * 重载方法匹配优先级
     */
    
    import java.io.Serializable;
    
    public class Overload {
      public static void sayHello(Object arg){
          System.out.println("hello Object");
      }
    
      public static void sayHello(int arg){
          System.out.println("hello int");
      }
    
      public static void sayHello(long arg){
          System.out.println("hello long");
      }
    
      public static void sayHello(Character arg){
          System.out.println("hello Character");
      }
    
      public static void sayHello(char arg){
          System.out.println("hello char");
      }
    
      public static void sayHello(char... arg){
          System.out.println("hello char ...");
      }
    
      public static void sayHello(Serializable arg){
          System.out.println("hello Serializable");
      }
    
      public static void main(String[] args) {
          sayHello('a');  // 输出hello char
      }
    }
    
    

    如果注释掉sayHello(char arg)方法,那么会输出hello int,这时发生了一次自动类型转化char('a')-->int(97)
    如果再注释掉sayHello(int arg),则会输出hello long,发生了两次次自动转型,char('a')-->int(97)-->long(97L)

    自动转型按照char > int > long > float > double是转型顺序进行匹配,但不会匹配到byte和short,因为char到byte和short的转型是不安全的

    sayHello(long arg)注释后,会输出hello Character,发生了一次自动装箱,'a'被包装为它的封装类型java.lang.Character

  • 动态分派
    动态分派与重写有着密切的关联

    运行期根据实际类型确定方法执行版本的分派过程称为动态分派

    这种多态性的根源在于虚方法调用指令invokevirtual

    package chapter8;
    
    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(); // print:man say hello
          woman.sayHello();   // print:woman say hello
          man = new Woman();
          man.sayHello(); // print:woman say hello
      }
    }
    
    

    invokevirtual指令的运行时解析过程
    1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
    2、如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常
    3、否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
    4、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

  • 单分派与多分派
    方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分成单分派和多分派。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择

package chapter8;

/**
 * 单分派、多分派演示
 */
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());  // print:father choose 360
        son.hardChoice(new QQ());   // son choose qq
    }
}

编译阶段的选择过程,也就是静态分派的过程。选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father::hardChoice(360)及Father::hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行,所以静态分派属于多分派类型

运行阶段中虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoice(new QQ())这行代码时,由于编译期已经决定目标方法的签名必须为hardChoice(QQ),所以参数的QQ是什么都不会构成任何影响,唯一可以影响虚拟机选择的因素只有该方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以动态分派属于单分派类型

如今的Java语言(Java12和预览版Java13)是一门静态多分派、动态单分派的语言

虚拟机动态分派的实现

虚方法表(Virtual Method Table,简称vtable)
存放着各个方法的实际入口地址,如果方法在子类没有重写,则子类虚方法表中的地址入口和父类相同方法的地址入口一致,都指向父类的实现入口。如果重写了方法,则子类虚方法表中的地址也会替换为指向子类实现版本的入口地址

方法表的结构

查虚方法表是分派调用的一种优化手段,除此之外,虚拟机为了进一步提高性能,还会使用类型继承关系分析、守护内联、内联缓存等多种非稳定的激进优化来争取更大的性能空间

动态语言支持

动态类型语言

动态语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的

运行时异常和连接时异常

  • 运行时异常就是指只要代码不执行到这一行就不会出现问题
  • 连接时异常就是即使导致连接时异常的代码放在一条根本无法被执行到的路径分支上,类加载时也照样会抛出异常

obj.println("hello world")
Java语言在编译期间就已将println(String)方法完整的符号引用生成出来,并作为方法调用指令的参数存储到Class文件中,例如下面这样
invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

这个符号引用包含了该方法定义在哪个具体的类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,Java虚拟机就可以翻译出该方法的直接引用

而EMCAScript等动态语言与Java有一个核心差异就是obj本身没有类型,而obj的值才具有类型,所以编译器在编译期最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的类型(即接收者不固定),“变量无类型而变量值才有类型”也是动态语言的一个核心特征

静态类语言能够在编译期确定变量类型,最显著的好处是编译器可以提供全面严谨的类型检查,这样与数据类型相关的潜在问题就能在编码时被及时发现,利于稳定性及让项目容易达到更大的规模
动态类语言在运行期才确定类型,这是可以为开发人员提供极大的灵活性,某些在静态类语言中要花大量臃肿代码来实现的功能,由动态类语言去做可能会很清晰简介,意味着开发效率的提升

后端编译与优化

后端编译:编译器无论在何时、何种状态下把Class文件转换成与本地基础设施(硬件指令集、操作系统)相关的二进制机器码,它都可以视为整个编译的后端

即时编译器

“热点代码”:某个方法或代码块的运行特别频繁

即时编译器:为了提高热点代码的执行效率,在运行时虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地优化,运行时完成这个任务的后端编译器

解释器与编译器

目前主流的商用Java虚拟机,内部都同时包含解释器与编译器。当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。当程序启动后,编译器把越来越多的代码编译成本地代码,减少解释器的中间损耗,获得更高的执行效率

当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提示效率

解释器可以作为编译器激进优化时后备的“逃生门”(情况允许,HotSpot虚拟机也会采用不进行激进优化的客户端编译器充当“逃生门”的角色)

让编译器选择一些不能保证所有情况都正确,但大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类后类型基础结构出现变化、出现“罕见陷阱”时可以通过逆优化退回到解释状态继续执行

解释器与编译器的交互

HotSpot虚拟机内置了两个(或三个)即时编译器

  • “客户端编译器”(Client Compiler):简称C1编译器
  • “服务端编译器”(Server Compiler):简称C2编译器,也叫Opto编译器
  • Graal编译器

HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数强制指定虚拟机运行在客户端还是服务端

三种模式

  • “混合模式”:编译器与解释器搭配使用
  • “解释模式”:使用参数“-Xint”强制虚拟机运行于“解释模式”,编译器完全不介入工作
  • “编译模式”:使用参数“-Xcomp”强制虚拟机运行于“编译模式”,优先采用编译方式执行程序,但解释器仍要在编译无法进行的情况下介入执行过程

分层编译
编译器编译本地代码需要占用程序运行时间,编译出优化程度越高的代码越耗时,解释器可能还要替代编译器收集性能监控信息,影响解释执行阶段速度

为了在程序启动响应速度于运行效率之间达到最佳平衡,加入了分层编译

解释器、客户端编译器和服务端编译器同时工作,用客户端编译器获取更高的编译速度,用服务端编译器获取更好的编译质量;解释执行的时候也无须额外承担收集性能监控信息的任务;服务端编译器采用高复杂度的优化算法时,客户端编译器可先采用简单优化来为它争取更多的编译时间

  • 0层:程序纯解释执行,并且解释器不开启性能监控功能
  • 1层:使用客户端编译器将字节码编译为本地代码运行,进行简单可靠的优化,不开启性能监控功能
  • 2层:仍使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能
  • 3层:仍使用客户端编译器执行,开启全部性能监控,除了2层的统计信息,还会收集分支跳转、虚方法调用版本等全部的统计信息
  • 4层:使用客户端编译器将字节码编译为本地代码,相比客户端编译器,服务端编译器会启用更多耗时更长的优化,还会根据性能监控信息进行一些不可靠的性能优化
分层编译的交互关系
编译对象与触发条件

热点代码主要有两类

  • 被多次调用的方法
  • 被多次执行的循环体

这两种情况编译的目标对象都是整个方法体,而不会是单独的循环体。第一种情况会以整个方法作为编译对象(标准的即时编译方式);后一种情况,也是以整个方法作为编译对象,但执行入口(从方法第几条字节码指令开始执行)稍有不同,编译时会出入执行入口点字节码序号(栈上替换)。

“栈上替换”(On Stack Replacement,OSR):编译发生在方法执行的过程中,即方法的栈帧还在栈上,方法就被替换了。

热点探测

  • 基于采样点的热点探测
    采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。
    优点:实现简单高效,很容易地获取方法调用关系(将调用对展开即可)
    缺点:很难精确地确认一个方法地热度,容易因为线程阻塞或别的外界因素的影响而扰乱热点探测
  • 基于计数器的热点探测
    虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
    优点:统计结果相对来说更加精确严谨
    缺点:实现起来更麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系

HotSpot虚拟机使用的是第二种热点探测方法

两类计数器

  • 方法调用计数器
    统计方法被调用的次数,默认阈值客户端模式下为1500次,服务端模式下为10000次(-XX:CompilerThreshold人为设定)

    方法调用计数器触发即时编译

    默认设置下。方法调用计数器统计的并不是方法被调用的绝对次数,而是相对的执行频率,即一段时间之内方法被调用的次数。当超过一定时间限度,方法调用次数未达到阈值,则该方法的调用计数器减少一半,这个过程为方法调用计数器热度的衰减(在收集收集时顺便进行,可用-XX:-UseCounterDecay关闭),这段时间为此方法统计的半衰周期(-XX:CounterHalfLifeTime设置,单位为秒)

  • 回边计数器
    统计方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令就称为“回边”,建立回边计数器统计的目的时为了触发栈上的替换编译

    回边计数器阈值计算公式

    • 客户端模式下:方法调用计数器阈值(-XX:CompilerThreshold) * OSR比率(-XX:OnStackReplacePercentage,默认值为933) / 100 ,都取默认值,那客户端模式下回边计数器阈值为13995
    • 服务端模式下:方法调用计数器阈值(-XX:CompilerThreshold) * (OSR比率(-XX:OnStackReplacePercentage,默认值为140) - 解释器监控比率(-XX:InterpreterProfilePercentage,默认值为33)) / 100,都取默认值,那服务端模式下回边计数器阈值为10700
    回边计数器触发即时编译

    回边计数器没有计数热度衰减,这个计数器统计的就是该方法循环执行的绝对次数,计数器溢出时,把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程

编译过程

默认条件下,虚拟机在未完成编译前,都仍按照解释方式执行代码,编译动作则在后台进行

禁止后台编译(-XX:-BackgroundCompilation),执行线程将会一直阻塞等待,直到编译过程完成再执行编译器输出的本地代码

  • 客户端编译器
    主要关注点在于局部性优化,放弃了许多耗时较长的全局优化收到

    高级中间代码(High-Level Intermediate Representation,HIR):与目标机器指令集无关的中间表示
    静态单分配(Static Single Assignment,SSA)形式:代表码值,一些在HIR的构造过程之中和之后进行的优化动作更容易实现
    低级中间代码(Low-Level Intermediate Representation,LIR):与目标机器指令集相关的中间表示

    客户端编译器架构
  • 服务端编译器
    专门面向服务端的典型应用场景,并为服务端的性能配置针对性调整的编译器,一个能容忍很高优化复杂度的高级编译器

    执行大部分经典的优化动作:无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等

    实施与Java语言特性密切相关的优化技术:范围排除消除、空值检查消除、等

    可能根据解释器或客户端编译器提供的性能监控信息进行一些不稳定的预测性激进优化:守护内联、分支频率预测等

编译器优化技术

方法内联

最重要的优化手段,被称为优化之母。它除了消除方法调用的成本之外,更重要的意义是为其他优化手段建立良好的基础。没有内联,多数其他优化都无法有效进行

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

对于虚方法,编译器静态地去做内联的时候很难确定应该使用哪个方法版本。为了解决这个问题,引入了类型继承关系分析(Class Hierarchy Analysis,CHA),这是整个应用程序范围内的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖率父类的某个虚方法等信息

通过内联缓存调用比用不内联的非虚方法调用,仅多了一次类型判断的开销而已

逃逸分析

最前沿的优化技术之一,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术

逃逸分析的基本原理为分析对象动态作用域,对象由低到高的不同逃逸程度

  • 从不逃逸
  • 方法逃逸:一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中
  • 线程逃逸:甚至可能被外部线程访问到,譬如赋值给可以在其他线程中访问到的实例变量

通过不同逃逸程度,给对象实例采取不同的优化

  • 栈上分配
    如果确定一个对象不会逃出线程之外,那让这个对象在栈上分配内存,对象所占用的内存空间就可以随栈帧出栈而销毁,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,不支持线程逃逸

  • 标量替换
    一个数据已经无法再分解成更小的数据来表示,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,这些数据就可以称为标量

    如果一个数据可以继续分解,则称为聚合量(Java中的对象)

    标量替换:把一个Java对象拆散,根据程序访问情况,将其用到的成员变量恢复为原始类型来访问

    一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不再创建这个对象,而是创建它的若干个被这个方法使用的成员变量来代替。

    将对象拆分后,除了可以让对象的成员在栈上分配和读写之外,还可以为后续进一步优化手段创建条件。它不允许对象逃逸出方法范围内

  • 同步消除
    如果能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉

-XX:+DoEscapeAnalysis:开启逃逸分析
-XX:+PrintEscapeAnalysis:查看逃逸分析结果
-XX:+EliminateAllocations:开启标量替换
+XX:+EliminateLocks:开启同步消除
-XX:+PrintEliminateAllocations:查看标量替换情况

公共子表达式消除

非常经典的、普遍用于各种编译器的优化技术

如果一个E之前已经被计算过了,并且先前计算到现在的E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式

  • 局部公共子表达式消除:优化仅限于程序基本快内
  • 全局公共子表达式消除:优化的范围涵盖了多个基本块

int d = (c * b) * 12 + a + (a + b * c)
进入虚拟机即时编译后,检测到b * c与c * b是一样的表达式,且b、c在计算期间值不变
int d = E * 12 + a + (a + E)
编译器还能来进行另外一种优化——化数为简ba
int d = E * 13 + a + a

数组边界检查消除

是即时编译器的一项语言相关的经典优化技术

如果有一个数组foo[i],则i必须满足“i >= 0 && i < foo.length”的访问条件,对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对拥有大量数组访问的程序代码,这必定是一种性能负担

数组访问发生在循环体之中,并且使用循环变量来进行数组的访问。如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间 [0 , foo.length] 之间,那么在循环中就可以把整个数组的上下界检查消除掉

数组边界检查优化尽可能把运行期检查提前到编译期完成

Java内存模型与并发

Java内存模型

主要目的:定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节

此处的变量包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数

主内存与工作内存

Java内存模型规定了所有变量都存储在主内存中。

每条线程还有自己的工作内存,线程的工作内存保存了该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据

内存间交互操作

主内存拷贝到工作内存、工作内存同步回主内存

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程的独占状态
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

在执行上述8种基本操作时必须满足如下规则

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现
  • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回内存
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须执行assign和load操作
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行assign和load操作
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)
对于volatile型变量的特殊规则

volatile是Java虚拟机提供的最轻量级的同步机制

当一个变量被定义成volatile后,具备两项特性

  • 保证此变量对所有线程的可见性
    即一条线程修改了这个变量的值,那么新值对于其他线程来说是立即可知的,不代表它是线程安全的

    不符合以下两条规则,仍然要通过加锁(synchronized、java.util.concurrent中的锁或原子类)来保证原子性

    • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
    • 变量不需要与其他的状态的变量共同参与不变约束
  • 禁止指令重排序化
    指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理

Java内存中对volatile变量定义的特殊规则
T:线程、V,W:volatile变量

  • 在工作内存中,每次使用V前都必须从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改
  • 每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改
  • volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同
针对long和double型变量的特殊规则

允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,“long和double的非原子协定”

原子性、可见性和有序性
  • 原子性
    由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个,基本数据类型的访问,读写都是具备原子性的(除了long和double)

    synchronized之间的操作也具备原子性

  • 可见性
    可见性是指一个线程修改了共享变量的值时,其他线程能够立即得知这个修改

volalite、synchronized、final能实现可见性

  • 有序性
    如果在本线程内观察,所有的操作都是有序的;如果一个线程中观察另一个线程,所有的操作都是无序的

volatite、synchronized保证线程之间操作的有序性

Java与线程

目前线程时Java里面进行处理器资源调度的最基本单位

线程的实现
  • 使用内核线程实现(1:1实现)
    直接由操作系统内核支持的线程

    这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上

    程序一般不会直接使用内核线程,而是使用内核线程(KLT)的一种高级接口——轻量级进程(LWP),轻量级进程与内核线程之间1:1的关系称为一对一的线程模型

  • 使用用户线程实现(1:N实现)
    一个线程只要不是内核线程,都可以认为是用户线程(UT)的一种

  • 使用用户线程加轻量级进程混合实现(N:M实现)
    内核线程与用户线程一起使用


Java线程调度

线程调度是指系统为线程分配处理器使用权的过程

  • 协同式线程调度
    线程的执行时间由线程本身来控制,线程执行完毕,通知系统切换线程

    优点:实现简单,一般没有线程同步问题
    缺点:线程执行时间不可控制

  • 抢占式线程调度
    每个线程由系统来分配执行时间,线程的切换不由线程本身来决定

    可以通过设置线程优先级来“建议”系统给某些线程多分配些时间。在两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行,但线程调度最终还是由操作系统说了算

    在Windows下设置线程优先级为1和2、3和4、6和7、8和9的效果是完全相同的

状态转换

六种线程状态:

  • 新建(New):创建后尚未启动的线程
  • 运行(Runnable):包括操作系统线程状态中的Running(正在执行)和Ready(等待操作系统为它分配执行时间)
  • `无限期等待(Waiting):不会被分配处理器执行时间,等待被其他线程显示唤醒。以下方法会陷入Waiting状态
    • 没有设置Timeout参数的Object::wait()方法
    • 没有设置Timeout参数的Thread::join()方法
    • lockSupport::park()方法
  • 限期等待(Timed Waiting):不会被分配处理器执行时间,不过无需等待被其他线程显示唤醒,在一定时间之后它们由系统自动唤醒,以下方法会陷入限期等待状态:
    • Threed::sleep()方法
    • 设置了Timeout参数的Object::wait()方法
    • 设置了Timeout参数的Thread::join()方法
    • LockSupport::parkNanos()方法
    • LockSupport::partUntil()方法
  • 阻塞(Blocked):线程被阻塞了,阻塞状态在等待着获取到一个排它锁,在程序等待进入同步区域的时候,线程将进入这种状态
  • 结束(Terminated):已终止线程的线程状态,线程已结束执行

Java与协程

最初多数的用户线程是被设计成协同式调度,所以也叫“协程”,它的主要优势是轻量

线程安全与锁优化

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

线程安全

安全程度
  • 不可变
    不可变的对象一定是线程安全的

    如果多线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不变的;如果是一个对象,那就需要对象自行保证其行为不会对其状态产生任何影响(String类、Long、Double等)

  • 绝对线程安全
    不管运行时环境如何,调用者都不需要任何额外的同步措施

  • 相对线程安全
    这个对象单次的操作是线程安全的

  • 线程兼容
    线程兼容指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用

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

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

最常见也是最主要的并发正确性保障手段

同步是指多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候的时候)线程使用。

互斥是实现同步的手段,临界区、互斥量和信号量是常见的互斥实现方式

互斥手段:

  • synchronized关键字

    • 被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况
    • 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。无法强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出
  • 重入锁ReentrantLock
    是Lock接口最常见的一种实现,它与synchronized一样可重入

与synchronized的区别

  • 等待可中断
    当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情
  • 公平锁
    多个线程在等待同一个锁时,必须按照申请锁的时间顺序来一次获得锁;而非公平锁则在锁被释放时,任何一个等待锁的线程都有机会获得锁。默认时非公平的,但可以通过带布尔值的构造函数要求使用公平锁
  • 锁绑定多个条件
    一个ReentrantLock对象可以同时绑定多个Condition对象,多次调用new Condition()方法即可
非阻塞同步

互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此也叫阻塞同步

非阻塞同步:基于冲突检测的乐观并发策略,不管风险先操作,有风险再进行其他补偿措施

无同步方案

如果一个方法不涉及共享数据,那就不需要任何同步措施

锁优化

自旋锁与自适应锁

自旋锁:如果两个或以上的线程同时并行执行,可以让后面请求锁那个线程“稍等一会”,但不放弃处理器的执行时间。为了让线程等待,我们只须让线程执行一个忙循环(自旋)

自选虽然避免了线程切换的开销,但是占用了处理器执行时间,如果等待时间短,那自旋等待的效果就会非常好。自旋的等待时间必须要有一定的限度,超过了限定次数没有成功获得锁,就要挂起线程。自旋次数默认值是10,可以用-XX:PreBlockSpin来自行修改

自适应自旋:由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

锁消除

虚拟机即使编译器在运行时检测到某段需要同步的代码根本不可能存在共享数据竞争而实施的一种对锁进行消除的优化策略

锁粗化

如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把锁同步的范围扩展(粗化)到整个操作序列的外部

轻量级锁
偏向锁

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

笔记来源于《深入理解Java虚拟机》周志明 著

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容