dubbo


《深入理解JVM虚拟机》

虚拟机字节码执行引擎

  • java语言中,javac编译器完成了程序代码经过此法分析,语法分析到查偶像语法树,再便利语法树生成线性的字节码至零六的过程,因为这一部分动作是再ajva虚拟机之外进行的,二解释器再java虚拟机内部,所以java程序的编译就是半独立的实现。

编译期优化

  • 编译器:
    • 前端编译器: java文件转为class文件,javac, ECJ。一般的编译是这。java语言编译器并非一个个地编译java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表中再进行编译。因此各个文件之间能够互相提供符号信息,无须使用预处理器。
    • 后端编译器(JIT):把字节码转为机器码的过程:hotspotVM的C1,C2编译器
    • 静态提前编译器:AOT编译器,直接把java文件编译成本地机器代码的过程
  • javac的编译过程
    • 解析和填充符号表->注解处理->分析与字节码生成
    • 解析与填充符号表
      • 词法分析:字符流转为TOKEN(关键字、变量名、字面量、运算符),例如int是一个token
      • 语法分析:语法树的每一个节点都表示一个语法结构,语法结构有包、类型、修饰符、运算符、接口、返回值、代码注释等
    • 注解处理
    • 语义分析与字节码生成
      • 标注检查
        • 检查变量是否被声明,变量与赋值之间的数据类型是否匹配,常量折叠
      • 数据集控制流分析
        • 局部变量与字段有区别,在常量池中没有CONSTANT_Filedref_info的符号引用,因此自然没有访问标志的值,甚至可能连名字都不能保留,所以class文件不可能知道一个局部变量是不是声明为final的,因此,将局部变量声明为final只是编译器的保障,控制分析的时候完成,在运行期没有影响。
      • 解语法糖
        • 语法糖是增加程序可读性的东西。语法糖指一种对语言的功能没有影响,但是更方便程序员使用,例如泛型、变长参数、自动装箱/拆箱等。虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。
      • 字节码生成
        • 把前面各个步骤所生成的信息(语法树、符号表)转化为字节码写入磁盘。
        • 少量添加和转换代码
  • java语法糖
    • 泛型与类型擦除
      • 所操作的数据类型被指定为一个参数
      • C#的泛型没有类型擦除,JAVA的泛型有类型擦除,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type)。并且在相应的地方插入了强制转型代码。因此,对于运行期的java语言来说,arraylist与arraylist 是同一个类。java中的泛型是**伪泛型。遭受众多的批评,通过擦除泛型丧失了一些泛型思想应有的优雅。**
      • 加入signature等新属性用于解决伴随泛型而来的参数类型的识别问题。signature中保存的参数类型不是原生类型,而实包括了参数化类型的信息。
    • 自动装箱、拆箱、遍历循环

运行期优化

  • 优化层次
    • Profiling-性能监控功能,如果不开启,则触发第0层编译,即程序解释执行
    • C1,简单可靠的优化。C2,耗时较长的优化,甚至不可靠的激进优化。
  • 编译对象与触发条件
    • 热点代码:被多次调用的方法,被多次执行的循环体。
    • 热点探测:基于采样的热点探测(检查线程的栈顶),基于计数器的热点探测(为每个方法建立计数器)。
  • 编译优化技术

    • 建立在机器码之上的优化

      • 内联。去除方法调用的成本(不必建立栈帧),方法内联膨胀之后可以便于在共呢个大范围上采取后续优化手段。只有使用Incokespecial指令调用的私有方法、实例构造器、父类方法以及使用invokestatic指令进行调用的静态方法才是在编译期进行解析的,除了上述4种方法之外,其他的java方法调用都需要在运行时进行方法接收者的多态选择。简言之,java中默认的实例方法是虚方法。虚方法需要在运行时确定。
      • 冗余访问消除。
      • 复写传播。消除等价变量。
      • 无用代码消除。
      • 公共子表达式消除。
      • 数组边界检查消除。java是一门动态安全的语言,对数组的读写访问不像C,C++那样本质上是裸指针操作。
      • 逃逸分析。逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义之后,它可能被外部方法引用,例如方法参数,方法逃逸;赋值给类变量或者可以在其他线程中访问的实例变量,线程逃逸。
        • 栈上分配,java堆中的对象对于各个线程都是共享和可见的,但是垃圾回收耗费时间,所以如果去欸但那个一个对象不会逃逸出方法之外,那让这个对象在栈上分配将会是一个很不错的注意。
        • 同步消除。
        • 标量替换。
    • java和C/C++编译器的对比

      • java编译器的劣势
        • JIT占用的是用户程序的运行时间。java虚拟机必须频繁的进行动态检查(空指针、数组上下界、类型转换)。java运行时堆方法接收者进行堕胎选择的频率高于C/C++。java语言时可以动态扩展的,因此运行时加载的新的类可能改变继承关系。java语言中对象的内存分配都是堆上进行的,只有方法发中的局部变量才能在栈上分配。
      • C/C++的劣势
        • C/C++别名分析更难。因为就算两个对象没有继承关系,其实例就也可能是同一个对象,指向同一块内存。
        • C/C++编译器所有优化都在编译器完成,以运行期性能监控为基础的优化措施都无法进行。

      java内存模型与线程

      • 硬件的效率与一致性
      • 基于高速缓存的存储交互引入“缓存一致性”问题。通过协议解决缓存一致性问题。例如:MSI, MESI, MOSI, Synapse, Firefly, Dragon Protocal等
      • 内存模型:在特定的操作协议下,堆特定的内存或高速缓存进行读写访问的过程抽象。
      • java内存模型
        • C、C++等直接用物理迎接和操作系统的内存模型,因此会由于不同平台上的内存模型的差异,导致程序移植性查。
        • java虚拟机规范中试图定义一种java内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下访问内存效果一致。
        • java内存模型
          • java内存模型主要定义程序中各个变量的访问规则(实例字段、静态字段、构成数组对象的元素),不包括局部变量和方法参数(线程私有)
          • 从定义上看:贮存对应java堆中堆像实例数据部分,工作内存(线程私有)对应于虚拟机栈中的那部分。
          • 从低层次上说,主内存直接对应于物理硬件的内存,工作内存可能优先存储与寄存器和高速缓存中
            • JMM内存间交互操作,8种原子操作(JSR-133已经放弃了用这8种操作定义JMM的访问协议,只是描述方式改变了,jmm并没有改变)
            • 主内存:lock, unlock, read, write
            • 工作内存:load,use,assign, store,
            • 如果把一个变量从主内存复制到工作内存,Read-load是顺序的,从工内同步回主内,store-write是顺序的。jmm只要求是顺序执行,而不保证连续执行。
            • 不允许一个线程assign之后不同步回主内存。
            • 不允许将没有assign操作的工作内存同步回主内存。
            • 不允许工作内存种直接使用一个未被初始化的变量。对一个变量进行use\store操作之前,必须先执行过assign\load操作。
            • 一个变量在同一个时刻只允许一条线程对其lock。
            • 对一个变量进行Lock操作,会清空工作内存中此变量的值,在使用这个变量之前,需要重新执行Load或者assign操作。
            • 不允许unlock一个未被Lock或者被其他线程Lock的变量。
            • 对一个变量执行unlock之前,必须把改变量同步回主内存。
        • volatile关键字。可见、有序。
          • java虚拟机提供的最轻量级的同步机制
          • volatile特性:①保证此变量对所有线程的可见性,当一个线程修改这个变量之后,新值对于其他线程而言是立即可知的。volatile变量在各个线程的工作内存中不存在一致性问题。但是volatile变量的运算在并发下不是安全的。②禁止指令重排序优化。
            • ①volatile只能保证可见性。适用符合以下场景
              • 运算结果并不依赖变量的当前值,或者能确保只有单一的线程修改变量的值
              • 变量不需要与其他的状态变量共同参与不变约束。
            • ②如果线程A中的变量没有被volatile修饰,则可能由于指令重排序优化将其位置提前,这样导致线程B使用该变量时是一个错误的信息。

            • volatile操作的本质是在汇编代码中加了一个lock指令,查询IA32手册,该指令的作用是使本CPU的cache写入内存。该写入工作也会引起别的CPU或者别的内核无效化其Cache.所以通过这个操作可以令前面volatile修饰的变量对其他CPU立即可见。但如果对加了volatile修饰的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量在缓存行的数据写回到主存。这时只是写回到主存,但其他处理器的缓存行中的数据还是旧的,要使其他处理器缓存行的数据也是新写回到主存的数据,就需要实现缓存一致性协议。

              即在一个处理器将自己缓存行的数据写回到系统内存后,其他的每个处理器就会通过嗅探在总线上传播的数据来检查自己缓存的数据是否已过期,当处理器发现自己缓存行对应的内存地址的数据被修改后,就会将自己缓存行缓存的数据设置为无效,当处理器要对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到自己的缓存行,重新缓存。

              总结下:volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:

              (1) 写volatile时处理器会将缓存写回到主内存。

              (2) 一个处理器的缓存写回到内存会导致其他处理器的缓存失效。

            • 指令重排序从硬件架构上讲是CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理,CPU需要正确处理指令依赖情况以保障程序正确性。

            • volatile变量读操作的性能消耗与普通变量没有差别,但是写操作可能慢一些,因为需要在本地代码中插入许多内存屏障指令。
        • synchronized关键字:一个变量在同一个时刻只允许一条线程对其进行Lock操作,对一个变量进行unlock操作之前,必须先把此变量同步回主内存。原子、可见、有序。
        • final关键字。可见。
      • 线程的实现
        • java API中,一个Native方法意味着这个方法没有使用或无法使用平台无关的手段来实现。
        • 通常实现线程的3种方式:
          • 使用内核线程实现
            • Kernel level thread,直接由操作系统内核支持的线程,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器,每个内核线程可以视为内核的一个分身。程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口,轻量级进程 Light weight process,LWP和KLT之间一对一支持。
            • 需要进行系统调用,在用户态和内核态之间来回切换,代价高。
          • 使用用户线程实现
            • 用户线程的建立、同步、销毁、调度完全在用户态中完成,不需要内核的帮助。操作系统只把处理器资源分配到进程,所以用户线程需要自己处理阻塞、将线程映射到其他处理器上等问题。所以现在一般都不用。
          • 使用用户线程加轻量级进程混合实现
      • java线程调度
        • 协同式线程调度、抢占式线程调度(系统分配执行时间)
        • java使用抢占式线程调度
      • 线程状态转换
        • 新建 start 运行
        • 运行 wait(),join(), park() 无限期等待,等其他线程唤醒 Notify() notifyall()
        • 运行 sleep(), wait, join, parkNanos, parkUntil 限期等待
        • 运行 synchronized 阻塞
        • 运行 run() 结束

线程安全与锁优化

  • 思想:
    • 数据&过程分开,面向过程的编程思想
    • 数据和行为都是对象的一部分,面向对象的编程思想
  • 线程安全的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调度方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
  • 不可变
    • String类是不可变对象。调用它的substring等方法不会影响它原来的值,只会返回一个新构造的字符串对象
    • 枚举类、Number的部分子类,Loing Double\BigInteger都是不可变类型。
  • 绝对线程安全
  • 相对线程安全:对这个对象单独的操作是安全的,但是对特定顺序的调用,可能需要在调用端使用额外的同步手段来保证调用的正确性。
    • Vector。线程安全的容器,其本身所有的方法都是synchronized同步方法。
  • reentrantLock 比 synchronized 高级的地方
    • 等待可中断
    • 公平锁
    • 锁绑定多个条件
  • 非阻塞同步
    • 乐观并发策略需要“硬件指令集的发展”才能进行,因为需要保证操作和冲突检测这两个步骤具备原子性。靠什么来保证?互斥同步会使其失去意义,只有在硬件层面保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:
      • 测试并设置(Test-and-Set)
      • 获取并增加(Fetch-and-Increment)
      • 交换(Swap)
      • 比较并交换(Compare-and-Swap,下文称CAS)、
      • 加载链接/条件存储(Load-Linked/Store-Conditional,下文称LL/SC)