什么是执行引擎?
在我理解就是,java编译之后,形成的字节码文件就需要执行引擎执行或解析成硬编码,也就是机器指令。
Java是半编译半解释型语言
1.javac编译,java运行
2.字节码解释器是解释运行的
3.运行期即时编译,编译成硬编码执行
JVM两种解释器
字节码解释器
字节码解释器做的事:字节码->c++代码->硬编码
执行效率低
CASE(_new): {
u2 index = Bytes::get_Java_u2(pc+1);
ConstantPool* constants = istate->method()->constants();
if (!constants->tag_at(index).is_unresolved_klass()) {
// Make sure klass is initialized and doesn't have a finalizer
Klass* entry = constants->slot_at(index).get_klass();
assert(entry->is_klass(), "Should be resolved klass");
Klass* k_entry = (Klass*) entry;
assert(k_entry->oop_is_instance(), "Should be InstanceKlass");
InstanceKlass* ik = (InstanceKlass*) k_entry;
if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
……
模板解释器
模板解释器做的事:字节码->硬编码
模板解释器底层实现步骤:
- 向内存申请一块可读可写可执行的内存 (mac系统不行)
- 将处理new字节码的硬编码拿过来(硬编码怎么拿到?) lldb 解析可执行文件
- 将硬编码放到申请的内存中
- 申请一个函数指针,用这个函数指针执行这块内存
- 使用的时候,调用这个函数指针
void TemplateTable::_new() {
transition(vtos, atos);
Label slow_case;
Label done;
Label initialize_header;
Label initialize_object; // including clearing the fields
Register RallocatedObject = Otos_i;
Register RinstanceKlass = O1;
Register Roffset = O3;
Register Rscratch = O4;
__ get_2_byte_integer_at_bcp(1, Rscratch, Roffset, InterpreterMacroAssembler::Unsigned);
__ get_cpool_and_tags(Rscratch, G3_scratch);
// make sure the class we're about to instantiate has been resolved
// This is done before loading InstanceKlass to be consistent with the order
// how Constant Pool is updated (see ConstantPool::klass_at_put)
__ add(G3_scratch, Array<u1>::base_offset_in_bytes(), G3_scratch);
__ ldub(G3_scratch, Roffset, G3_scratch);
__ cmp(G3_scratch, JVM_CONSTANT_Class);
__ br(Assembler::notEqual, false, Assembler::pn, slow_case);
……
三种运行模式
JIT即时编译为什么会这么快?
原因是运行期的热点代码编译与缓存
什么叫热点数据?
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (On StackReplacement)编译
什么是热度衰减?
当超过一定的时间限度, 如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用热度衰减
JVM中有两种即时编译器,就诞生了三种运行模式
-Xint:纯字节码解释器模式
-Xcomp:纯模板解释器模式
Xmixed:字节码解释器+模板解释器(默认)
两种即时编译器
- c1
在-client模式下,默认启动c1,特点:
1、需要收集的数据较少,即达到触发即时编译的条件较宽松
2、自带的编译优化优化的点较少
3、编译时较C2,没那么耗CPU,带来的结果是编译后生成的代码执行效率较C2低 - c2
-server模式启动。特点:
1、收集数据较多
2、编译很费CPU
3、编辑的优化点比较多
4、代码执行效率比较高 - 混合编译
目前的-server模式启动,已经不是纯粹只使用C2。程序运行初期因为产生的数据较少,这时候执行C1编译,程序执行一段时间后,收集到足够的数据,执行C2编译器
c1和c2编译的代码就是给模板解释器执行的!
即时编译触发条件
在client模式下1500次
在server模式下10000次
触发即时编译的最小单位是代码段,最大单位是方法
查看值
java -client -XX:+PrintFlagsFinal -version | grep CompileThreshold
这个阈值可以通过虚拟机参数-XX :CompileThreshold来人为设定
热点代码缓存区
热点代码缓存是保存在方法区的,这块也是调优需要调的地方
server 编译器模式下代码缓存大小则起始于 2496KB
client 编译器模式下代码缓存大小起始于 160KB
查看数据
java -XX:+PrintFlagsFinal -version | grep InitialCodeCacheSize
逃逸分析
什么是逃逸分析?
可以把逃逸和分析拆开
逃逸是指变量逃出方法内,逃到方法外或逃出线程外。
也可以先说什么是不逃逸,肯定不逃逸的是局部变量,那么可以说除了局部变量,其他的就是逃逸的。比如参数,全局变量,返回值等
分析:基于不逃逸的基础上进行优化
public class EscapeTest {
public static Object globalVariableObject;
public Object instanceObject;
public void globalVariableEscape(){
静态变量,外部线程可见,发生逃逸
globalVariableObject = new Object();
}
public void instanceObjectEscape(){
赋值给堆中实例字段,外部线程可见,发生逃逸
instanceObject = new Object();
}
public Object returnObjectEscape(){
返回实例,外部线程可见,发生逃逸
return new Object();
}
public void noEscape(){
synchronized (new Object()){
//仅创建线程可见,对象无逃逸
}
仅创建线程可见,对象无逃逸
Object noEscape = new Object();
}
public void test(){
未逃逸
int a = 0;
int b = 1;
}
}
基于逃逸分析诞生的三种优化策略:
栈上分配
栈上分配主要是指在Java程序的执行过程中,在方法体中声明的变量以及创建的对象,将直接从该线程所使用的栈中分配空间。 一般而言,创建对象都是从堆中来分配的,这里是指在栈上来分配空间给新创建的对象。
如何证明?
生成了一个对象100w次,现在看堆区是不是有100w个,不发生gc的情况下,如果没有,就存在栈上分配。
标量替换
也就是在栈上把基本数据类型,对象结构不可再分的直接替换掉,而不是去找内存地址
锁消除
这个就很好理解了。这里发现对象没有逃逸。可能把关键词去掉,也可能是让它没有竞争对手。让同步失效,这样可以支持更大的并发
public void noEscape(){
synchronized (new Object()){
//仅创建线程可见,对象无逃逸
int a =0;
a =10;
}
仅创建线程可见,对象无逃逸
Object noEscape = new Object();
}