天天看点

假装会优化之jvmjvm

jvm

  • jvm优化首先要知道一些类加载,编译器.堆外内存.计数器的概念和优化点

类加载

  • 类加载的过程需要连接和初始化最后才会使用
    • 连接:
      • 1:验证:检查规范
      • 2:准备 给类的静态变量赋初始值,final修饰的直接赋值
      • 3:解析:符号引用->直接引用
    • 初始化:jvm首先执行构造方法,编译器在把java文件编译成class文件时,就把静态变量,静态方法.静态代码块一起组成 ,会按照源码的顺序来执行

      子类初始化会先执行父类的方法,再执行自己的

      父类静态方法或静态代码块(按照源码顺序)->子类静态方法或静态代码块->父类构造方法->子类构造方法

      在初始化中,如果是新建一个实例对象,则会调用方法,并执行对应的构造方法

编译

  • 即时编译:在字节码转换成机器码的过程中,还存在着一个编译,即时编译

    刚开始是由解释器来进行编译的,虚拟机发现某些方法经常被执行,就成为热点代码,为了提高效率,(JIT)即时编译器会将代码编译成相关的机器码,然后优化之后,存放到内存

分层编译

  • java7之前有c1和c2两个即时编译器,之后就有了分层编译
    • c1:关注局部性的优化,适用于执行时间较短或对启动性能有要求的程序
    • c2:适用于执行时间长或对峰值性能有要求的程序
  • 分层编译:
    • 0层:程序由解释器执行,默认开启监控功能(profiling),如果不开启,触发第二层编译
    • 1层:c1编译,将字节码编译成本地代码,进行简单可靠的优化,不开启profiling
    • 2层:c1编译,开启profling.仅执行带方法调回次数和循环汇编执行次数profiling的c1编译
    • 3层:c1编译,执行所有带profiling的c1编译
    • 4层c2编译,将字节码编译成本地代码,会启用一些编译耗时较长的优化,会根据新能监控信息进行一些不可靠的激进优化
  • java8默认开启了分层编译,如果想要开启c2,关闭分层编译(-XX:-TieredCmpilation),如果只想用c1,可以在打开分成编译的同时,使用参数:

    -XX:TieredStopAtLevel=1.

    -Xint强制运行于只有解释器的编译模式,

    -Xcomp强制运行只有JIT的编译模式下(无分层模式)

计数器

  • 热点检测:JIT优化的条件就是HotSpot的热点检测,热点检测是基于计数器的热点探测,采用这方法的虚拟机会每个方法建立计数器统计方法的执行次数,超过阈值,就是热点方法
  • 计数器分类:
    • 1:方法调用计数器:统计方法被调用的次数,c1模式下是1500次,c2是10000次,可以使用-XX:CompileThreshold来设定,在分层编译的情况下,-XX:CompileThreshold失效,根据待编译的方法数和编译线程数来动态调整,当方法计数器和回边计数器和超过方法计数器阈值时,会触发JIT编译器
    • 回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令成为回边,在不开启分层的情况下,c1默认为13995,c2为10700,通过-XX:OnStackRelacePercentage=x来设置,在分层编译中,阈值失效,通过当前编译的方法数,和编译线程数来动态调整
    • 回边计数器是为了触发OSR编译,栈上编译,在循环周期较长的代码段中,当循环 达到阈值之后,JIT编译器会将这段代码编译成机器语言缓存,在该循环中,会将执行代码替代,执行缓存的机器语言

编译优化

  • 编译优化技术
    • 1:方法内联:调用方法需要经历压栈和出栈,只要执行前保护现场并记忆执行的地址,执行后恢复现场,会产生一定时间和空间上的开销

      如果方法体代码不是很大,有频繁调用的方法来说,空间和时间消耗会很大,

      方法内联的优化行为是吧目标方法的代码赋值到发起调用的方法之中,避免发生真是的方法调用

    • jvm会自动识别热点方法,并使用方法内联进行优化,通过-XX:CompileThreshold来设置热点方法的阈值,但是如果方法体太大,jvm也不会执行内联操作,方法的大小阈值可以设置参数来优化
    • 精华藏执行的方法,
      • 方法体小于325字节都会进行内联,通过

        -XX:MaxFreqlnlineSize=n来设置

      • 方法大小小于35字节才会进行内联,通过-XX:MaxInlineSize=n来设置
      • -XX:+PrintCompilation // 在控制台打印编译过程信息
      • -XX:+UnlockDiagnosticVMOptions // 解锁对 JVM 进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对 JVM 进行诊断
      • -XX:+PrintInlining // 将内联方法打印出来
  • 总结:
    • 通过设置jvm参数减少热点阈值或增加方法体阈值,以便更多的方法可以进行内联,但是需要更多的内存
    • 避免在一个方法中写大量的代码,使用小方法体
    • 尽量使用final,privete,static关键字修饰方法,编程方法因为继承,会需要额外的类型检查

逃逸分析

  • 逃逸分析:判断一个对象是否被外部方法引用或外部线程访问的分析技术,编译器会根据逃逸分析的结果对代码进行优化
  • 栈上分配,对象创建是在堆中分配内存的,如果逃逸分析发现一个对象只在方法中使用,就会将对象分配到栈上(暂时未优化)
  • 锁消除:如果加锁的对象或变量是在局部方法中,处于只能被当前线程访问,无法被其他线程访问的,所以JIT会对这个进行锁消除

标量替换

  • 标量替换:
    • 如果一个对象不会被外部访问,且这个对象可以被拆分,当真正执行是,可能不创建这个对象,而是直接创建他的成员变量来代替,对象拆分后,分配成员变量在栈或寄存器上,原本的对象就无须分配内存空间了,这个编译优化叫做标量替换

      -XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8 默认开启,其它版本未测试)

      -XX:-DoEscapeAnalysis 关闭逃逸分析

      -XX:+EliminateLocks 开启锁消除(jdk1.8 默认开启,其它版本未测试)

      -XX:-EliminateLocks 关闭锁消除

      -XX:+EliminateAllocations 开启标量替换(jdk1.8 默认开启,其它版本未测试)

      -XX:-EliminateAllocations 关闭就可以了

堆外内存

  • 为什么使用堆外内存
    • 1、减少了垃圾回收

        使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。

    • 2、提升复制速度(io效率)

        堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了这个操作。

  • 堆外内存创建有两种方式:
    • 1.使用ByteBuffer.allocateDirect()得到一个DirectByteBuffer对象,初始化堆外内存大小,里面会创建Cleaner对象,绑定当前this.DirectByteBuffer的回收,通过put,get传递进去Byte数组,
    • 2.序列化对象,Cleaner对象实现一个虚引用(当内存被回收时,会受到一个系统通知)当Full GC的时候,如果DirectByteBuffer标记为垃圾被回收,则Cleaner会收到通知调用clean()方法,回收改堆外内存DirectByteBuffer

关于gc和回收器的选择

假装会优化之jvmjvm

继续阅读