当前位置: 首页 > 科技观察

Java代码如何在机器上运行?

时间:2023-03-14 16:44:14 科技观察

本文转载自微信公众号《编了一个程序》,作者Yasinx。转载本文请联系编辑程序公众号。概述计算机能识别的是机器指令码,简称机器码。机器码是二进制的,计算机可以直接识别,但与人类语言相差太大,不易被人理解和记忆。后来又诞生了各种高级语言。人们用高级语言编写程序,然后将程序解释或编译成机器代码。例如,Python是一种解释型语言。Python程序的源代码不需要编译,直接从源代码运行程序即可。Python解释器将源代码转换为字节码,然后将编译后的字节码转发给Python虚拟机(PVM)执行。C语言是典型的编译型语言,需要用编译器编译成机器码。比如我们平时用gcc编译C语言程序:$gcchello.c#compile$./a.out#executehelloworld!那么Java是解释型语言还是编译型语言呢?“Java既有编译型语言的特点,又有解释型语言的特点。”程序员写完Java程序后,需要用javac编译成JVM可以使用的字节码类文件。然后JVM加载这个class文件,一个一个解释执行。在运行过程中,一些热点代码会被即时编译器编译成机器码。SourceCodetoByteCodeJava语言的源代码是一个后缀为.java的文件。当然,还有很多其他的高级语言也是建立在JVM之上的,比如groovy、kotlin等,源码是给人看的,便于阅读、理解和维护。源码编译得到字节码,供JVM使用,易于理解和识别。字节码以.class为后缀,其格式是JVM的一套计划。字节码与文档相比人类几乎无法理解,但比Java代码更难理解。Java不同于Python。Python不需要编译字节码文件(当然Python也提供了这个操作)。编译是一个自动过程,一般你不会关心它的存在。Java会先编译字节码文件,让JVM直接读取字节码文件,这样可以节省加载模块的时间,提高效率。同时字节码的形式也增加了逆向工程的难度,可以保护源代码(当然也可以反编译)。熟悉JVM的朋友都知道,它有个“类加载过程”,可以说是老套路了,经常被面试官问到。类加载过程实际上是指JVM从读取一个类文件到准备类,最后销毁类的整个过程。所以“class文件其实是基于“类”的,与java文件有些区别。”如果我们在一个Java文件中声明了多个类,用Javac编译的时候就会发现多个类文件。比如我们声明一个One.java文件:publicclassOne{publicclassOneInner{}privateclassOnePrivateInner{}publicstaticclassOneStaticInner{}privatestaticclassOneprivateStaticInner{}}classTwo{}用javac编译后会有6个class文件?$ls'One$OneInner.class''One$OneStaticInner.class'One.classTwo.class'One$OnePrivateInner.class''One$OneprivateStaticInner.class'One.java字节码转机器码字节码的加载和使用前面提到JVM会加载class文件,并且那么加载的Java类就会存放在方法区(MethodArea)中。从指定类的main方法为入口开始运行。实际运行时,虚拟机执行方法区的代码,JVM使用堆和栈来存放运行时数据。每当进入一个方法时,Java虚拟机都会在当前线程的栈中生成一个栈帧,用于存放局部变量和字节码操作数。这个堆栈帧的大小是预先计算好的。当退出一个方法时,无论是正常返回还是异常返回,Java虚拟机都会“弹出当前线程的当前栈帧”并丢弃。Java虚拟机需要将字节码翻译成机器码才能让机器执行。这个过程有两种形式,一种是解释执行,将字节码一个一个翻译成机器码并执行;另一种是即时编译(JIT),就是将“在一个方法中”包含的所有字节码编译成机器码,然后执行。分层编译这两种编译方式如何配合呢?HotSpot虚拟机包括多个即时编译器C1、C2和Graal。其中,Graal是一个实验性的即时编译器,可以通过参数-XX:+UnlockExperimentalVMOptions-XX:+UseJVMCICompiler启用,并替代C2。C1和C2各有优缺点,适用于不同的场景。在Java7之前,只能选择一种编译器。C1编译速度很快,但是生成代码的执行效率一般。常用于执行时间短或对启动性能有要求的程序。需要很长时间执行或需要峰值性能的程序通常在服务器端使用。其实C1对应的参数是client,C2对应的参数是server,也符合他们的应用场景。Java7引入了分层编译的概念,结合了C1的启动性能优势和C2的峰值性能优势。C1和C2编译出来的机器码是不一样的。C2代码的执行效率比C1代码高30%以上。机器代码越快,编译时间越长。分层编译是一种折中的方式,既能满足一些不太热的代码在短时间内编译出来,又能满足热代码的最佳优化。热码如何确定热码?JVM会收集方法的运行时信息,主要包括调用次数和回环次数。当“方法调用次数与回环次数之和超过指定阈值”时,??会触发即时编译。->回环次数可以简单理解为方法内部代码的循环次数,例如方法内部有for循环或者while循环。<-在分层编译出现之前,这个阈值是由参数-XX:CompileThreshold指定的。使用C1时,值为1500;使用C2时,该值为10000。启用分层编译时,JVM使用另一个阈值系统。在这个系统中,阈值的大小是动态调整的。JVM将阈值与一些系数s相乘。该系数与当前要编译的方法数正相关,与编译线程数负相关。编译线程默认情况下,编译线程的总数根据处理器的数量进行缩放。Java虚拟机将这些编译线程按照1:2的比例分配给C1和C2(至少各1个)。例如,对于四核机器,编译线程总数为3,包括一个C1编译线程和两个C2编译线程。->当机器资源太少时,可能每台1个线程。<-可以看到有arthas的编译线程:^arthas^可以看到他们的ID是-1,优先级也是-1。我们自己创建的线程优先级是0~10,所以编译线程的优先级会更高。一句话概括,Java程序是如何在机器上运行的?首先,Java程序员编写Java代码,然后Java代码会被编译成一个class文件,多个class文件会被打包成一个jar包或者war包。然后JVM加载类文件,然后首先将其解释为字节码执行。程序运行一段时间后,JVM会不断通过方法的调用次数和循环次数来判断一个方法是否是热代码。如果是,它会使用分层编译,通过编译线程编译成字节码,在机器上运行。