合网站 - 百度,重庆最新消息今天,微信小程序开发费用,开发邦平台类编译加载执行过程
如下图所示#xff0c;一个Java代码从编译到运行大抵会经历以下几个过程。具体每个过程笔者会在下文站展开讨论。
类编译
首先是类编译阶段#xff0c;这个阶段会将Java文件变为class文件#xff0c;这个class文件包含一个常量池和方法表集合#xf…类编译加载执行过程
如下图所示一个Java代码从编译到运行大抵会经历以下几个过程。具体每个过程笔者会在下文站展开讨论。
类编译
首先是类编译阶段这个阶段会将Java文件变为class文件这个class文件包含一个常量池和方法表集合而方法表集合里面会包含方法访问权限、返回类型、JVM执行指令以及属性集合等信息。
类加载
对于没有加载的类JVM就会拿着这个class文件进行类加载JDK自带的本地方法在双亲委派机制下会用根加载器Bootstrp loader进行加载而JDK扩展方法则会由扩展加载器ExtClassLoader 我们应用程序自己写的方法则是由系统加载器AppClassLoader 完成加载。
完成加载后常量池或者每个类的字段描述符、方法描述符等信息都会加载到JVM的方法区同时会在堆区生成一个代表这个类的java.lang.Class对象作为这个类的各种数据的访问入口。 类连接
类连接就是验证、准备、初始化3个过程了。
验证:验证类符合 Java 规范和 JVM 规范在保证符合规范的前提下避免危害虚拟机安全。准备: 为类的静态变量分配内存初始化为系统初始值。private final static int value123在这一步就完成空间分配和初始赋值为0。而private final int num123则会在这一步直接赋值为123。因为它是一个常量。解析:将编译器每个类的符号引用(包括类和接口的全限定名、类引用、方法引用以及成员变量引用等)等信息转为直接引用(JVM可直接获取的内存地址或指针)。
类初始化
JVM会执行构造器的cinit收集所有类、方法、静态变量的初始化静态变量赋值、静态代码块、静态方法然后按顺序从上到下执行。
注意笔者说的按顺序从上到下这就意味的静态变量的完成初始化后的结果是以最后一个初始化语句为准。如下所示
赋值语句在后结果为1 public class Main {static {num 2;}private static int num 1;public static void main(String[] args) {System.out.println(num);//1}
}静态代码块在后结果为2
public class Main {private static int num 1;static {num 2;}public static void main(String[] args) {System.out.println(num);//1}
}即时编译(重点)
在初始化阶段完成后执行引擎不断将调用到的字节码翻译成机器码交由计算机执行。Java字节码转为机器码之间还有一步转换我们称之为既时编译。 最初Java字节码文件是直接通过解释器 Interpreter 解释为机器码直接运行的。 后来设计者们考虑到某些执行频率比较高的代码我们可以称之为热点代码可以进行某些机制进行优化(例如对代码逻辑进行优化缓存到本地内存中)。 所以我们如今编写的Java代码若执行频率非常高的话就会被判定为热点代码那么即时编译器就会对这类代码进行逻辑优化编译为最优的本地机器码保存到内存中。 著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。 即时编译器类型有哪些
在HotSpot 虚拟机中内置了两个JIT编译器分别为
C1编译器:主要关注点在于局部性优化常用于那些执行时间短或者要求快速启动的应用程序例如GUI应用程序。C2编译器:常用于长期运行且对峰值性能有高要求的服务器。
所以我们也称C1编译器和C2编译器为 Client Compiler或者Server Compiler。 在 Java7 之前需要根据程序的特性来选择对应的 JIT虚拟机默认采用解释器和其中一个编译器配合工作。 Java7 引入了分层编译这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势我们也可以通过参数 “-client”“-server” 强制指定虚拟机的即时编译模式。分层编译将 JVM 的执行状态分为了 5 个层次 第 0 层程序解释执行默认开启性能监控功能Profiling如果不开启可触发第二层编译 第 1 层可称为 C1 编译将字节码编译为本地代码进行简单、可靠的优化不开启 Profiling 第 2 层也称为 C1 编译开启 Profiling仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译 第 3 层也称为 C1 编译执行所有带 Profiling 的 C1 编译 第 4 层可称为 C2 编译也是将字节码编译为本地代码但是会启用一些编译耗时较长的优化甚至会根据性能监控信息进行一些不可靠的激进优化。 在 Java8 中默认开启分层编译-client 和 -server 的设置已经是无效的了。如果只想开启 C2可以关闭分层编译-XX:-TieredCompilation如果只想用 C1可以在打开分层编译的同时使用参数-XX:TieredStopAtLevel1。 我们可以使用java -version查看当前编译的编译模式可以看到笔者服务器的JVM使用的就是混合编译模式 [rootiZ8vb7bhe4b8nhhhpavhwpZ ~]# java -version
java version 1.8.0_202
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)如果我们想强制运行JIT编译模式也可以使用java -Xint -version如果我们想强制运行JIT编译模式也可以使用
java -Xcomp -version热点探测了解过吗(重点)
HotSpot 虚拟机判定热点代码是基于两种计数器进行的分别是方法调用计数器Invocation Counter和回边计数器Back Edge Counter只有执行代码符合他们的标准且达到他的设置的阈值时才会进行JIT编译优化。
方法调用计数器
这个计数器工作机制非常好理解当某个方法执行次数超过阈值时就会触发JIT编译优化这个阈值我们可以通过jinfo查看,如下所示可以看到笔者JVM设置的方法调用计数器判定是否是热点代码的条件为调用次数达到10000次。
[rootxxx~]# jinfo -flag CompileThreshold 2341
-XX:CompileThreshold10000回边计数器
在字节码遇到控制流后跳转的操作我们称之为回边。回边计数器判定代码为热点代码的条件是:一个代码在循环体内达到回边计数器要求的阈值而这个阈值我们也可以通过jinfo查看
[rootxxx~]# jinfo -flag OnStackReplacePercentage 2341
-XX:OnStackReplacePercentage140当这段代码被判定为热点代码时JVM就会进行一种栈上编译的优化操作它会将这段代码编译为最优逻辑保存到本地内存在执行循环体的期间直接使用缓存中的机器码。
JIT自动进行的编译优化技术(重要)
方法内联
我们都知道方法调用会经历一个压栈和出栈的操作执行调用方法时会将地址转移到存储该方法的起始地址上待调用结束后在返回原来的位置。 这就意味着一个方法调用另一个方法时就需要保存当前方法执行位置栈上压入被调用方法执行完成后恢复现场继续执行之前执行的方法。因此方法调用期间是有一定的时间和空间的开销的。
所以JIT会对那些方法调用方法非常频繁的代码执行方法内敛如下所示:
private int add1(int x1, int x2, int x3, int x4) {return add2(x1, x2) add2(x3, x4);
}
private int add2(int x1, int x2) {return x1 x2;
}最终会被优化为
private int add1(int x1, int x2, int x3, int x4) {return x1 x2 x3 x4;
}但是方法内敛优化也是有条件的,除了必须是热点代码(达到XX:CompileThreshold的阈值)以外还要达到以下要求:
对于经常执行的方法方法体要小于325字节这个字节数可以通过-XX:MaxFreqInlineSizeN来调整。对于不经常执行的方法方法体要小于35字节这个字节数可以由-XX:MaxInlineSizeN 来调整。
我们不妨看一段代码可以看到add1执行了1000000次
public class JVMJit {public static void main(String[] args) {for (int i 0; i 1000000; i) {add1(1, 2, 3, 4);}}private static int add1(int i, int i1, int i2, int i3) {return i i1 i2 i3;}}我们可以对这段程序添加这样一段参数查详情-XX:PrintCompilation -XX:UnlockDiagnosticVMOptions -XX:PrintInlining
他们的含义分别是
-XX:PrintCompilation // 在控制台打印编译过程信息
-XX:UnlockDiagnosticVMOptions // 解锁对 JVM 进行诊断的选项参数。默认是关闭的开启后支持一些特定参数对 JVM 进行诊断
-XX:PrintInlining // 将内联方法打印出来
可以看到这段代码被判定为热点代码说明他已经被JVM优化了
所以这就要求我们平时写代码时:
方法体尽可能小尽可能使用private、final、static修饰避免一些没必要的类是否继承等相关检查。
栈上分配
在将栈上分配前我们需要先了解一个叫逃逸分析Escape Analysis的技术。 逃逸分析就是判断当前操作的对象是否有被外部方法引用或外部线程访问的一种技术若逃逸分析判定当前对象并没有被其他引用或者线程使用到的话某些机制就可以开始进行优化比如我现在要说的栈上分配。
我们都知道创建一个对象都是在堆上分配的假如这个对象使用封闭GC就会将其回收而创建和回收这一来一回的操作也是有一定开销的。而栈则不一样它使用的引用或者各种变量随着调用的结束就消亡。
而栈上分配就是抓住这一特点当他经过逃逸分析技术发现这个对象并没有被外部引用且仅在当前线程使用那么它就会将该对象分配在栈上。如下面这样一段代码:
public static void main(String[] args) {for (int i 0; i 200000 ; i) {getAge();}
}public static int getAge(){Student person new Student( 小明 ,18,30); return person.getAge();
}static class Student {private String name;private int age;public Student(String name, int age) {this.name name;this.age age;}...get set
}但是在 HotSpot 中暂时没有实现这项优化。随着即时编译器的发展与逃逸分析技术的逐渐成熟相信不久的将来 HotSpot 也会实现这项优化功能。
锁消除
同样在逃逸分析某些没有被外部方法或者其他线程引用的情况下会将某些锁消除。例如下面这段代码实际上你在运行时可以发现StringBuffer 和StringBuilder 性能上没有什么区别这正是因为锁消除为我们做的优化工作。
public static String getString(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();}标量替换
当一个代码的对象在方法上可以拆分并且代码仅仅是对这个对象的变量进行各种操作的话编译器可能会执行标量替换如下所示 public void foo() {TestInfo info new TestInfo();info.id 1;info.count 99;...//to do something}由于上述代码仅仅是创建一个对象后操作对象的变量实际上这个工作似乎和对象没有任何关联编译器识别到这点之后就不去创建没必要的对象进而使用标量替换的方式将对象的成员变量放到栈上避免没必要的对象创建和销毁。 public void foo() {id 1;count 99;...//to do something}我们可以通过设置 JVM 参数来开关逃逸分析还可以单独开关同步消除和标量替换在 JDK1.8 中 JVM 是默认开启这些操作的。
-XX:DoEscapeAnalysis 开启逃逸分析jdk1.8 默认开启其它版本未测试
-XX:-DoEscapeAnalysis 关闭逃逸分析-XX:EliminateLocks 开启锁消除jdk1.8 默认开启其它版本未测试
-XX:-EliminateLocks 关闭锁消除-XX:EliminateAllocations 开启标量替换jdk1.8 默认开启其它版本未测试
-XX:-EliminateAllocations 关闭就可以了