执行引擎

执行引擎

“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力。

其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
1、JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。

2、那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

 

工作过程
1、执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。

2、每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。

3、当然方法在执行的过程中,执行引擎可能会通过存储在局部变量表中的对象引用准确定位到存储在JAVA堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。

 

解释器
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

JIT编译器
JIT (Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

 

C1
-client:指定Java虚拟机运行在client模式下,并使用C1编译器;

C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。

①方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程

②去虚拟化:对唯一的实现类进行内联

③冗余消除:在运行期间把一些不会执行的代码折叠掉

 

C2
-server:指定Java虚拟机运行在Server模式下,并使用C2编译器。

C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。

①标量替换:用标量值代替聚合对象的属性值

②栈上分配:对于未逃逸的对象分配对象在栈而不是堆

③同步消除:清除同步操作,通常指synchronized

 

比较
C2编译器启动时长比c1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。

问题
既然有了JIT编译器,为什么还需要解释器来拖累程序的执行性能呢?

当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。

编译器想要发挥作用,把代码编译成本地代码,需要一定的执行时间。但搬移为本地代码后,执行效率高。

 

执行方式
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。

 

热点代码
HotSpot VM采用的热点探测方式是基于计数器的热点探测。

采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器和回边计数器

①方法调用计数器用于统计方法的调用次数

②回边计数器则用于统计循环体执行的循环次数

当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值(默认10000次 -XX:CompileThreshold 修改)。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

 

StringTable
String在JDK9的变化
jdk8中的String内部存储从char[]变为byte [],节省了空间。

http://openjdk.java.net/jeps/254

 

String:代表不可变的字符序列。简称:不可变性。

①当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。

②当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,

不能使用原有的value进行赋值。

③当调用string的replace ()方法修改指定字符或字符串时,也需要

重新指定内存区域赋值,不能使用原有的value进行赋值。

通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

字符串常量池中是不会存储相同内容的字符串的。

 

内存分配
在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。

常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。

①直接使用双引号声明出来的string对象会直接存储在常量池中。

比如:string info = "atguigu . com" ;

②如果不是用双引号声明的string对象,可以使用string提供的intern ()方法。这个后面重点谈

 

字符串的拼接
1、常量与常量的拼接结果在常量池,原理是编译期优化

2、常量池中不会存在相同内容的常量。

3、只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder

4、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

 

G1的String去重
当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。

如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。

使用一个hashtable来记录所有的被string对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。

如果存在,string对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。

如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。

 

垃圾回收
什么是垃圾
在运行程序中没有任何指针指向的对象。

 

为什么需要GC
如果不对内存进行垃圾回收,内存迟早会被耗尽

 

不通语言的垃圾回收
C/C++
使用手工回收垃圾,用new进行内存申请,delete进行垃圾回收。

 

缺点:如果开发人员忘记回收垃圾,进程产生内存泄漏,最终会导致程序崩溃。

 

JAVA
对不再使用的对象进行自动回收

缺点:对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。

堆是垃圾回收器的重点区域

频繁回收年轻代,较少回收老年代,基本不动元空间

 

垃圾回收算法
标记阶段
回收垃圾前,要先判断哪些对象需要回收
引用计数算法
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点
1、它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2、每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3、引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

 

Python
Python使用的是引用计数算法
Python如何解决循环引用?
①手动解除:很好理解,就是在合适的时机,解除引用关系。
②使用弱引用weakref, weakref是Python提供的标准库,旨在解决循环引用。

posted @ 2023-02-09 17:30  sugarstar  阅读(44)  评论(0)    收藏  举报