这期我们来聊java代码是如何运行的?(一)

你好,我的名字是TT。

在本期中,我们来谈谈java代码是如何工作的。大家都知道java运行在JVM上,那么它是如何结合操作系统来控制那些硬件设备的呢?

其实,如果我们想知道这个问题,我们可以追溯一行代码的整个生命周期来解释,我把它抽象成这样五个步骤。

图片[1]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

首先,将这行代码编译成字节码,然后由JVM通过类加载器加载,然后由解释器解释成机器码cpu中跟踪指令后继地址的寄存器,解释器分配执行这条指令所需的资源——主要是内存。然后 CPU 执行指令并将结果写回内存。

接下来,我们将逐步分析它。

首先,java是一门高级语言。这种语言不能直接在硬件上运行。它必须运行在能够识别java语言特性的虚拟机上,java代码必须转换成java编译器能够识别的虚拟机。指令序列,也称为 java 字节码。

图片[2]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

那么 .out.(“Hello world”) 编译后的字节码是什么样的呢?最左边一列是偏移量,中间一列是虚拟机读取的字节码,最右边一列是高级语言翻译。

使用字节码,通过类加载器加载 java 虚拟机。加载之后,我们通过解释器把它解释成汇编指令,最后再翻译成CPU可以识别的机器指令,那么机器指令转换成汇编语言是什么样子的呢?

图片[3]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

我们可以看到中间是机器码,第三列是对应的机器指令,最后一列是对应的汇编代码。

图片[4]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

解释器由软件实现。它将字节码转换为汇编指令,使得相同的Java字节码可以在不同的硬件设备上运行,而汇编指令到机器指令的转换是直接由硬件实现的,所以速度会更快。

图片[5]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

JVM为了提高运行效率,会一次性将一些热代码编译成机器指令然后执行cpu中跟踪指令后继地址的寄存器,即即使编译对应解释执行,即使编译后的机器码存放在某个地方称为缓存,这块内存属于堆外。内存。如果这块内存不够,即使编译器不再编译,也可能导致程序运行速度变慢,这也是我们排查性能问题的一个点。

代码被转换成指令,指令必须有上下文环境才能执行。这些环境包括内存资源,例如指令寄存器、数据寄存器和堆栈空间。

图片[6]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

程序加载到内存后,指令就在内存中。指令的指针寄存器IP指向内存中要执行的下一条指令的地址。CPU的控制单元根据IP寄存器的指针将主存中的指令加载到指令寄存器中。指令寄存器也是一个存储设备,但它集成在CPU内部。指令从主存到达CPU后,只是一串二进制字符串,需要解码器进行解码。解码后根据运算类型从主存中获取操作数,调用运算单元进行计算。

图片[7]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

事实上,我们的数据主要存储在内存中,但是 CPU 的计算速度比主存的访问速度快很多倍,所以中间有多层缓存。例如,当 CPU 有一条指令要在主存中取某个值时,CPU 会首先根据该值在主存中的位置来判断它是否已经在缓存中。在缓存中。

这个地方会涉及到一个知识点,也就是从主存中读取的时候,不会只是读取一个值,而是会取出并缓存一个一定长度的值,因为它会假设既然你已经读取了某个value 一个位置的值和与该位置相邻的值也会被读取。就像我们用SQL查询id=800的行时,虽然返回了id=800的行,但实际上在读取这行记录的时候,就是这行记录所在的数据页上的所有数据被存储。把它放在记忆中。也许下次你去查询id=801的记录时,你只是打了缓存,不需要去磁盘查。

图片[8]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

所以我们知道一个缓存行可能会缓存多个字段的值。如果一个进程改变了其中一个值,整个缓存就会失效,缓存行上的其他值会从内存中重新读取。因此,一些对内存要求很高的应用程序希望避免这种情况。例如,他们将使用对象填充,以便字段的值可以占据整个缓存行。

图片[9]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

那么,有了执行环境,我们的代码什么时候会被执行呢?我们知道CPU一上电就会不停的取指令和操作,不断的循环往复,那么你可能会问,我的代码什么时候执行?

实际上,CPU为每个进程分配一个时间片,在这个时间片内执行相应的进程指令,在这个时间片之后执行其他进程。进程中指令的执行顺序取决于每条指令的执行。指向下一个命令的位置。当然,进程内的一些操作也会主动放弃CPU的执行权限,比如等待IO操作。

图片[10]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

所以为了让一个进程中的指令执行效率更高,我们可以让其他线程获得CPU的执行权限,在一个线程等待IO的时候继续执行。如果你的任务都是计算任务,基本不主动释放CPU,那么单核机器上就没有必要开启多个线程了。如果有大量的IO操作,多线程的效果会更好。

接下来,我们分析一下代码执行时内存是如何分配的。JVM启动时,会生成一个进程。虽然多个进程会共享一块物理内存,但每个进程都会有自己独立的内存空间。

图片[11]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

当我们同时启动多个JVM,执行.out.(new())时,默认会打印出这个对象的地址,最后发现打印的全是java.lang.@。

也就是说,多个JVM进程返回的内存地址是相同的,也就是说每个进程都有独立的地址空间。

图片[12]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

实际上,每个进程都会自己维护一个虚拟内存。虚拟存储允许每个进程认为它垄断了整个内存空间。这样做的好处是每个进程都有一致的虚拟地址空间。简化内存管理,进程不需要与其他进程竞争内存空间,因为是独占的,这也保护了各自的进程不被其他进程破坏。

每个进程在申请内存时,都会维护虚拟内存和物理内存的映射关系,防止其他进程占用自己的内存,而这个虚拟内存空间可能会超过物理内存,超过物理内存就可能发生数据溢出。存储在磁盘上。

图片[13]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

页表保存了虚拟地址和物理地址的映射关系。页表是一个数组,每个元素都是一个页面的映射关系。这种映射关系可能是与主存的,也可能是与磁盘的。页表存储在主存储器中。也可以存储在缓冲区中。

我们将存储在缓存中的页表称为 TLAB。好了,我们现在知道了JVM进程中的内存地址是如何与物理内存相关联的,那么具体一行java代码无非就是读取某个属性的值,对该值进行操作,然后写入新值回到某个值。一个属性,那么我们如何读取一个属性的值呢?

图片[14]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

我们可以参考JDK中反射的实现,也就是说,当我们获得一个Field对象时,可以通过set()方法或者get()方法来设置和读取一个属性的值。它首先需要获取该属性的相对对象。初始位置的偏移量,如果持有对这个对象的引用,就可以得到该对象在虚拟内存中的起始地址,然后我们可以根据该属性的偏移量得到该属性的虚拟内存地址,而然后查询页表得到物理内存的起始地址,然后根据这个属性的类型得到对应长度的数据。

图片[15]-这期我们来聊java代码是如何运行的?(一)-4747i站长资讯

写作也是如此。加载类时确认属性相对于对象初始位置的偏移量。它与类绑定,所以如果一个对象只有一个属性,如果它没有被压缩,那么这个对象就没有被压缩。占128位,这个属性的偏移量可能是128。如果有多个属性,JVM会对属性重新排序,对齐内存,保证对象占用的大小是8的倍数。另外一个作用是确保某个属性的值都在一个CPU缓存行中,否则某个属性的值将部分在缓存行A中,部分在缓存行B中。

本期分享到此结束。希望本期分享可以帮助到你。请在评论区给我留言。

文章来源:http://baijiahao.baidu.com/s?id=1721030050991812376&wfr=spider&for=pc

------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享