详解Java代码执行机制

本文深入探讨了Java代码的执行机制,包括javac编译过程、类加载机制和类执行机制。在类加载中,详述了LinkageError和ClassCastException的出现场景。类执行机制中,介绍了栈顶缓存、HotSpot的编译策略(C1和C2),以及逃逸分析相关的优化技术。文章还提到了反射执行的原理,并分析了HotSpot为何不启动时即编译成机器码的原因。
摘要由CSDN通过智能技术生成

1.Java代码的执行机制

1.Java源码编译机制

  1. javac将java源码编译为class文件的步骤:
    1.分析和输入到符号表 Parse分析过程所做的为词法和语法分析,词法分析是将代码字符串转变为token序列;语法分析是根据语法由token序列生成抽象语法树。 Enter输入过程是符号输入到符号表,通常包括确定类的超类型和接口,根据需要添加默认构造器,将类中出现的符号输入类自身的符号表中。
    2.注解处理 该步骤主要用于处理用户自定义的annotation,可能带来的好处是基于annotation来生成附加的代码或进行一些特殊的检查,从而节省一些共用代码的编写。
    3.语义分析和生成class文件 analyse分析步骤基于抽象语法树进行一系列的语义分析, 在完成语义分析后,开始生成class文件。

2.类加载机制

  1. 类加载机制是指.class文件加载到JVM,并形成Class对象的机制,之后就可对Class对象进行实例化并调用。
  2. LinkageError:该异常在自定义ClassLoader的情况下更容易出现,主要是因为此类已经在ClassLoader加载过了,重复地加载会造成该异常,因此要注意避免在并发的情况下出现这样的问题。
    由于JVM的这个保护机制,使得在JVM中没办法直接更新一个已经load的Class,只能创建一个新的ClassLoader来加载更新的Class,然后将新的请求转入该ClassLoader中来获取类,这也是JVM不好实现动态更新的原因。
  3. ClassCastException:在JDK5支持泛型后,合理使用泛型可相对减少此异常的触发。(比如2个A对象由不同的ClassLoader加载)

3.类执行机制

  1. 栈顶缓存: 在方法的执行过程中,可以看到很多操作要将值放入操作数栈,这导致了寄存器和内存要不断地交换数据,而栈顶缓存就是将本来位于操作数栈顶的值直接缓存在寄存器上,这对于大部分只需要一个值的操作而言,无需将数据放入操作数栈,可直接在寄存器计算,然后放回操作数栈。
  2. HotSpot在执行过程中对执行频率高的代码进行编译,对执行不频繁的代码则继续采用解释的方式。在编译上提供了2种模式,client compiler和server compiler。
  3. client compiler又称为C1,较为轻量级,只做少量性能开销比高的优化,它占用内存较少,适合于桌面交互式应用在寄存器分配策略上,JDK6以后采用的为线性扫描寄存器分配算法。其他优化如下:
    1.方法内联:把调用到的方法的指令直接植入当前方法中。
    2.去虚拟化:在装载class文件后,进行类层次的分析,如发现类中的方法只提供一个实现类,那么对于调用了次方法的代码,也可进行方法内联,从而提升执行的性能。
    3.冗余消除:在编译时,根据运行时状况对代码进行折叠或消除。
  4. server compiler又称为C2,较为重量级,采用了大量的传统编译优化技巧来进行优化,占用内存相对会多一些,适合于服务端。 和C1不同的主要是寄存器分配策略及优化的范围,寄存器策略上C2采用的为传统的图着色寄存器分配算法。 由于C2会收集程序的运行信息,因此其优化的范围更多在于全局的优化,而不仅仅是一个方法块的优化。 逃逸分析是优化的基础,逃逸分析是指C2在编译时会做以下的优化。
    1.标量替换: 用标量替换聚合量。好处是如果创建的对象并未用到其中的全局变量,则可以节省一定的内存。而对于代码执行而言,由于无需去找对象的引用,也会更快一些。
    2.栈上分配: 如果对象没有逃逸,那么C2会选择在栈上直接创建Point对象实例,而不是在堆上,好处是更加快速,另一方面是回收时随着方法的结束,对象也被回收了。
    3.同步削除: 如果发现同步的对象未逃逸,那也没有同步的必要了。
    除了基于逃逸分析的这些外,C2还会基于其拥有的运行信息来做其他的优化,例如编译分支频率执行高的代码。
    运行后C1,C2编译出来的机器码如果不再符合优化条件,则会进行逆优化,也就是回到解释执行的方式。
  5. 除了C1,C2外,还有一种特殊的OSR(On Stack Replace),它只替换循环代码体的入口,而C1,C2替换的是方法调用的入口,因此OSR编译后会出现的现象是方法的整段代码被编译了,但只有在循环代码体部分才执行编译后的机器码,其他部分则仍然是解释执行方式。
  6. HotSpot之所以未选择在启动时即编译成机器码,有以下的原因:
    1.静态编译并不能根据程序的运行状况来优化执行的代码,C2这种方式是根据运行状况来进行动态编译的,在静态编译的情况下无法实现的,给C2收集运行数据越长的时间,编译出来的代码越好。
    2.解释执行比编译执行更节省内存。
    3.启动时解释执行的启动速度比编译再启动更快。
    但程序在未编译期间解释执行的方式会比较慢,因此需要取一个权衡值,在HotSpot主要依据方法上的2个计数器是否超过阈值,其中一个计数器为调用计数器,即方法被调用的次数;另一个计数器为回边计数器,即方法中循环执行部分代码的执行次数。
    CompileThreshold:方法被调用多少次后,就编译为机器码。
    OnStackReplacePercentage: 用于计算是否触发OSR编译的阈值,
    InterpreterProfilePercentage: 当方法上的回边计数器到达这个值时,即触发后台的OSR编译。
  7. 反射执行: 基于反射可动态调用某对象实例中对应的方法、访问查看对象的属性等。
    调用privateGetDeclaredMethods来获取Class中的所有方法,在privateGetDeclaredMethods对Class中所有方法集合做了缓存,第一次会调用本地方法来获取。
    扫描方法集合列表中是否有相同方法名及参数类型的方法,如果有,则复制生成一个新的Method对象返回,如果没有,则继续扫描父类,父接口是否有该方法,如果仍然没找到放出,则抛出NoSuchMethodException.
    校验Class是否为public类型,如果权限不足,则直接抛出SecurityException。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值