从我们编写的java文件到机器上跑起来的系统,中间经历了什么呢?
首先java文件经过类编译成为.class文件,class文件经过类加载加载到虚拟机中,在虚拟机中经过链接、初始化而
成为字节码,最后经过运行时编译成为机器码
类编译使用JDK自带的javac工具完成编译即可成为.class文件,class文件可以分为几项信息:
魔数,版本号,常量池,字段表集合,方法表集合,我们重点关注常量池和方法表集合,常量池主要是
类文件里面的字面常量及符号引用,字面常量主要是字符串常量、final常量、基本类型例如-127-128的整型,
符号引用主要是类和接口的全限定名、类引用、方法引用及成员变量引用
方法表集合主要是方法的字节码、访问权限public private等等、方法名索引、jvm指令、属性集合等等
类加载过程就是一个类被创建了,然后虚拟机以前没加载过,那么就会通过类加载器加载进来,类的信息会存放在JVM的方法区里面
类链接过程呢,又分为三部分:验证、准备、解析
验证阶段:就是jvm想执行java文件,那么你这个文件我得先验证一些你是否符合jvm规范、java规范
准备阶段:试想一些,我们这是在载入一个类,那么最优先的,肯定是属于这个类的直接属性对不对,那就是静态变量啊,
然后我们可以把静态变量分成两种,一种就是单纯的静态变量,另一种就是带final的静态变量啊,这两个的区别是什么呢?
带final的,说明他不可变了,那么我们这时候就可以给他分配内存空间了并且可以直接赋值了,而静态变量呢,先给他内存空间
就好了,赋值先赋值成类型的初始值
解析阶段:我们刚刚上面说过了,我们在方法区里面放了类的信息嘛,然后我们只是给静态属性分配了内存空间而已,而我们方法区
里面那些符号引用并还没有处理呢,所以这个阶段就是把那些符号引用变成直接引用,就是把他们变为 jvm能识别调用的内存地址或者指针
初始化阶段:这个阶段,jvm会执行一个clinit的方法,就是我们的静态代码块、静态方法、静态变量赋值语句收集在一起,然后执行我们代码
的赋值,而不是类属性的初始值了,如果你这个类有继承关系的话,jvm会先执行父类的再执行子类的,老子优先,很合理。然后因为每个类
都是独一无二的只有一个,所以得保证并发问题,保证只有一个线程完成这个工作
至此为止,我们的java文件就变成在字节码运行在jvm里面了,剩下的工作就是变成机器码在机器里面执行了,在这过程中,jvm还藏着
一层编译,就是即时编译
即时编译:虚拟机中的字节码是由解释器转变为机器码的。这时想一下我们开发过程中碰到的场景,就是经常访问的数据我们肯定会加一层
缓存,以便加速访问。那么jvm也一样,虚拟机发现有一些代码或者代码块运行的很频繁,那么可想而知,我们可以做些特殊处理以便加速
访问,所以在JIT编译器会把这些代码编译成机器码并做相关优化然后存在内存里,怎么区分哪些代码是热点代码呢?
热点探测:就是jvm会给每个方法放两个计数器,一个是方法调用计数器,一个是回边计数器。方法调用计数器就是如同他的命名一样,就是
统计方法的执行次数,达到设定的阈值便认为是热点代码,或者是当配置的阈值失效时,根据当前待编译的方法数以及编译线程数来动态调整,
回边计数器是为了触发 OSR On StackReplacement编译 栈上编译,就是碰到一些循环代码,当循环次数达到阈值时,把这些代码编译成机器
语言并缓存,以提高执行效率,接下来我们看看JIT具体为我们做了哪些优化
方法内联:方法的调用,需要经历在虚拟机栈的压栈和出栈,本来我们程序在执行,碰到要执行方法了,我们就得去方法的内存地址那边,
把方法执行完再回去,这就要求我们得先记住我们原来怎么样的执行完方法回去还得恢复回来然后继续执行,如果一个方法执行次数很多次,
我们得来来回回好多次,产生时间和空间开销也不小,而且感觉起来不怎么必要对不对,方法内联的就是把目标方法的代码复制到
发起调用的方法里面然后执行,举个例子你们就清楚了:比如一个方法A,有四个入参 1234,目的是想完成1+2+3+4,然后A方法里面
又调用了B、C两个方法,B方法执行1+2,C方法执行3+4,蛮傻的,那么方法内联就会把A方法变成1+2+3+4,然后因为这涉及到内存的复制嘛,
如果方法体太大的话,就不会这么做了,经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,我们可以通过 -
XX:MaxFreqInlineSize=N 来设置大小值;不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通过 -
XX:MaxInlineSize=N 来设置置大小值。
所以先小总结一下就是:我们可以通过设置 JVM 参数来控制热点阈值,以便更多的方法可以执行内联,,然后我们coding的时候,尽量方法写得小一点,多几个,还有就是尽量用 final、private、static 关键字修饰方法,因为比如public的方法可以被继承,会有额外的类型检查开销
逃逸分析:Escape Analysis就是判断一个对象有没有被外部的线程执行到,没有的话,可以做如下优化
栈上分配:我们创建一个对象,是在堆里面分配内存的,当不再使用时,会经历垃圾回收机制回收掉,相对于堆分配而言,在栈里面的对象的
创建和销毁显然成本更低,简而言之,一个是全局的一个是局部的,这时经过逃逸分析发现一个对象只有在这个方法里面使用,那么就是把这个
对象分配在栈里面。
锁消除:同样的道理,假如一个对象加了锁,但是其实他就在这个方法里面使用,显然毫无竞争关系,那么JIT会把这个对象的方法锁消除掉,
举个例子:StringBuffer,你只在一个方法里面对方法里面的字符串在append,锁显然没必要。
标量替换:同样的道理,比如一个studnt对象,里面只有一个int的age,我们对这个age进行操作,JIT编译就不会去创建这个student对象,
而是直接创建一个int的age然后在栈里面操作