万字详解JVM,让你一文吃透( 六 )


数组边界检查消除因为Java会自动检查数组越界,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑是一种性能负担 。
如果数组访问发生在循环之中 , 并且使用循环变量来进行数组访问,如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在数组区间内,那么整个循环中就可以把数组的上下界检查消除掉,可以节省很多次的条件判断操作 。
方法内联内联消除了方法调用的成本 , 还为其他优化手段建立良好的基础 。
编译器在进行内联时,如果是非虚方法,那么直接内联 。如果遇到虚方法,则会查询当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,那么也可以内联 , 不过这种内联属于激进优化,需要预留一个逃生门(Guard条件不成立时的Slow Path),称为守护内联 。
如果程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接受者的继承关系发现变化的类,那么内联优化的代码可以一直使用 。否则需要抛弃掉已经编译的代码,退回到解释状态执行,或者重新进行编译 。
逃逸分析逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法里面被定义后 , 它可能被外部方法所引用,这种行为被称为方法逃逸 。被外部线程访问到,被称为线程逃逸 。
如果对象不会逃逸到方法或线程外,可以做什么优化?

  • 栈上分配:一般对象都是分配在Java堆中的,对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据 。但是垃圾回收和整理都会耗时,如果一个对象不会逃逸出方法,可以让这个对象在栈上分配内存,对象所占用的内存空间就可以随着栈帧出栈而销毁 。如果能使用栈上分配,那大量的对象会随着方法的结束而自动销毁 , 垃圾回收的压力会小很多 。
  • 同步消除:线程同步本身就是很耗时的过程 。如果逃逸分析能确定一个变量不会逃逸出线程,那这个变量的读写肯定就不会有竞争,同步措施就可以消除掉 。
  • 标量替换:不创建这个对象,直接创建它的若干个被这个方法使用到的成员变量来替换 。
Java与C/C++的编译器对比
  1. 即时编译器运行占用的是用户程序的运行时间 , 具有很大的时间压力 。
  2. Java语言虽然没有virtual关键字,但是使用虚方法的频率远大于C++,所以即时编译器进行优化时难度要远远大于C++的静态优化编译器 。
  3. Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,使得全局的优化难以进行,因为编译器无法看见程序的全貌,编译器不得不时刻注意并随着类型的变化,而在运行时撤销或重新进行一些优化 。
  4. Java语言对象的内存分配是在堆上,只有方法的局部变量才能在栈上分配 。C++的对象有多种内存分配方式 。
物理机如何处理并发问题?运算任务,除了需要处理器计算之外,还需要与内存交互,如读取运算数据、存储运算结果等(不能仅靠寄存器来解决) 。
计算机的存储设备和处理器的运算速度差了几个数量级,所以不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache) , 作为内存与处理器之间的缓冲:将运算需要的数据复制到缓存中,让运算快速运行 。当运算结束后再从缓存同步回内存,这样处理器就无需等待缓慢的内存读写了 。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性 。在多处理器系统中,每个处理器都有自己的高速缓存,它们又共享同一主内存 。当多个处理器的运算任务都涉及同一块主内存时,可能导致各自的缓存数据不一致 。
为了解决一致性的问题,需要各个处理器访问缓存时遵循缓存一致性协议 。同时为了使得处理器充分被利用,处理器可能会对输出代码进行乱序执行优化 。Java虚拟机的即时编译器也有类似的指令重排序优化 。
Java 内存模型什么是Java内存模型?Java虚拟机的规范 , 用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的并发效果 。
Java内存模型的目标?定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节 。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题 。

推荐阅读