JVM之方法执行和方法调用

Java虚拟机栈

参数设置

-Xss 
//设置栈空间的大小如果太大会使得线程太少 并发能力下降 默认1M

栈帧

  • 每个栈帧等于一个方法 一个线程可以有多个栈帧
  • 当前栈帧:当前执行方法的栈帧

栈帧存储什么数据

1.局部变量表 
	a.方法里定义的变量 
	b.方法的入参定义的变量 
	c.是一组变量值存储空间 数组 
	d.存储容量:变量槽为最小单位 编译时就被定义好 
2.操作数栈    
	a.作用:比如两个数相加需要将两个数放入到操作数栈中相加以后把结果在放入到操作数栈中再取出来压入到家局部变量表中 
	b.存储容量:编译时就被定义好   
3.动态连接和方法返回地址(作为了解) 
	a.符号引用变成直接引用的过程

栈异常

  1. 如果线程请求的栈深度大于虚拟机所允许的那么会抛出StackOverflowError异常
  2. 当虚拟机栈无法申请到足够的内存时会抛出OutofMemoryError

本地方法栈

C++ 实现
调用底层实现 比如线程唤醒或睡眠
初衷融于C++

方法执行

字节码指令集

  • 说白了就是吧你的java代码翻译成一行一行系统认得的操作码
  • Java字节码指令由操作码和操作数构成
iconst_0 操作码
bipush 10 操作码 + 操作数

JVM程序执行流程

流程详解:.java文件->javac命令->Java字节码文件.class->类加载技术->将.class文件通过类加载技术加载到JVM内存运行时数据区->判断是否缓存编译代码如果是直接执行机器码指令->如果不是再判断是否是热点代码->如果不是那么执行Java解释器逐行解释->如果是则触发即时编译器->转换成对应的机器码交由操作系统进行操作
ps:其中有多处名词需要详细解释 请往下看
在这里插入图片描述

Java解释器

  • 逐行解释 每次都会进行解释 将字节码对应起来一步一步执行
  • 不占用内存

即时编译器

  • 别称:JIT编译器
  • 属于动态编译:即在运行时进行编译
  • 将方法体编译成机器码后缓存起来-缺点:占用内存 需要向编译器提交请求,第一次需要消耗时间。空间换时间的做法
  • 静态编译:C、C++

热点代码

  • 被多次调用的方法
  • 被多次执行的循环体
  • **编译器都是以整个方法作为编译对象 **

热点代码检测方式

  • 采样:(对栈顶的方法进行采样 作为了解)
  • 方法调用计数器方法:常用JVM采用的是这种(JVM会为每个方法建立一个计数器 达到了次数就会进行)
计数器方法
  • 在JVM client模式下的阈值是1500次
  • 在JVM server模式下的阈值是10000次
  • 可以通过-XX:CompileThreshold设置
  • 从流程图也可以说明JIT编译器的步骤是比较多的因此是需要消耗一定的时间
    在这里插入图片描述

问题:为什么需要使用两者并存的架构

编译器和解释器的特点:

  • 当一次执行时 解释器的执行效率更高
  • 当多次执行时 编译器的执行效率更高
  • 编译器第一次执行时需要时间开销
  • 两者是互补的.首先当程序需要迅速启动和执行时解释器发挥作用,省去编译时间。随着时间的推移编译器逐渐发挥作用

JIT优化

JIT优化项有非常多 列举几个面试常常问的

  • 公共子表达式消除(将子表达式消除以节省计算时间)
  • 方法内联(将方法调用直接使用方法体的代码进行替换)
  • 方法逃逸分析

方法逃逸分析

概念

当一个对象在方法种被定义后,它可能被外部方法所引用.从而通过逃逸分析来判断新的对象的引用是否要将这个对象分配到堆上.如果非必要 那么会在栈上分配对象以优化节省内存空间
参数:
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析
jdk1.7默认开启

逃逸分析类型:

  1. 全局变量赋值逃逸
public static Object object;
public void test1(){
   object = new Object();
}
//当调用test1方法时全局变量object被初始化了,后续会被方法体以外的线程所访问到.
  1. 方法返回值逃逸
public Object test2(){
   return new Object();
}
  1. 实例引用发生逃逸
public void test3(){
    User user = null;
    test4(user);
}

public void test4(User user){
    user = new Object();
}
//当调用test3方法时user作为参数传输test4方法,user3方法user=null,但是当执行完test4之后它已经在堆中开辟了一块内存空间此时称之为实例引用发生逃逸
  1. 线程逃逸
//直接将Stringbuffer返回它的作用域就不只是方法内部,虽然是个局部变量,
//但是有可能被外部线程访问到.这称之为线程逃逸
public static StringBuffer craeteStringBuffer(String s1,String s2) {
       StringBuffer sb = new StringBuffer();
       sb.append(s1);
       sb.append(s2);
       return sb;
   }
//如果不像逃逸可以这么写:
public static String createStringBuffer(String s1, String s2) {
       StringBuffer sb = new StringBuffer();
       sb.append(s1);
       sb.append(s2);
       return sb.toString();
   }

对象的栈上内存分配

也可以理解为JIT优化之方法逃逸带来性能上的提升测试

public class EscapeAnalysisTest {
   private int age = 19;

   public static void main(String[] args) {
      long a1 = System.currentTimeMillis();
      for (int i = 0; i < 1000000; i++) {
         alloc();
      }

      // 查看执行时间
      long a2 = System.currentTimeMillis();
      System.out.println("cost " + (a2 - a1) + " ms");

      // 为了方便查看堆内存中对象个数,线程sleep
      try {
         Thread.sleep(1000000);
      } catch (InterruptedException e1) {
         e1.printStackTrace();
      }
   }

   // 测试方法逃逸的方法
   //分析一下alloc方法中的user对象会不会发生逃逸.并不会 因为方法题外并没有其他引用
   //前提是测试的方法中是不会发生逃逸的!
   private static void alloc() {
      User user = new User();
   }

   // 准备创建的对象
   static class User {}
}

测试一:

关闭逃逸分析:参数:
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError

结果:产生100w的user对象
在这里插入图片描述
测试二:
开启逃逸分析:参数:
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError

结果:产生12w的user对象
在这里插入图片描述
总结:经过JIT优化以后,堆内存分配的对象数量从100w降到了12W.这属于非常大的提升.也可以通过GC的次数来分析.开启逃逸分析后GC的次数会明显减少.正是因为堆上分配优化成了栈上分配.所以GC的次数有了明显的减少

如果有人问你所有的对象和数组都会在堆中分配内存吗?

答案:不一定.随着JIT编译器的发展在编译期间如果JIT经过逃逸分析发现有些对象没有逃逸出方法.那么有可能会对其进行优化成栈空间内存分配.但也不是绝对的,上述案例也展示了,开启逃逸分析并不是所有的User对象都在栈上分配.

同步锁消除

public class TestLockEliminate {
   public static String getString(String s1, String s2) {
      StringBuffer sb = new StringBuffer();
      sb.append(s1);
      sb.append(s2);
      return sb.toString();
   }

   public static void main(String[] args) {
      long tsStart = System.currentTimeMillis();
      for (int i = 0; i < 100000000; i++) {
         getString("TestLockEliminate ", "Suffix");
      }
      System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
   // System.gc();
   }
}
同步锁消除测试
//测试1
-XX:+DoescapeAnalysis -XX:-EliminateLocks

//测试2
-XX:+DoescapeAnalysis -XX:+EliminateLocks

//查看执行的时间 适当可以将循环次数调大

总结:
同样要基于逃逸分析,只有当加锁的变量不会发生逃逸时,没必要加锁时,JIT编译期间才可以将同步锁去除.减少加锁和释放锁造成的开销.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值