如何理解Java代码是如何运行的

写了几年的Java代码的人,大多数也难以真正说清楚,一行代码是怎么被操作系统执行的,我也说不清楚。这次在极客时间的每日一课中,看到了《结合操作系统,如何理解一行Java代码是怎么运行的》 作者董鹏 的一篇文章,非常优秀,忍不住整理下,分享给大家。

要想了解一行代码最终是怎么运行的,可以跟踪代码的整个生命中期。一行Java代码,首先被编译为字节码,被JVM虚拟机通过类加载器进行加载,然后这里面还有些安全校验,校验通过后,通过解释器进行解释成机器码,然后分配内存等资源,最后执行将操作结果写好回内存。
用图标识:


Java代码执行过程

这里面只是粗略的步骤,有兴趣的可以看看《深入理解Java虚拟机》里面讲解的更详尽些。

一 编译

为什么需要编译,因为Java是一名高级语言,不能直接运行在硬件上,它生来就是为了跨平台的,通过JVM虚拟机来屏蔽底层硬件的差异,Java的编译就是将Java的源代码,编译为JVM虚拟机可以识别的字节码。

一个Hello world程序编译为字节码如下:


图片来自视频

中间的就是Java编译出来的字节码,JVM加载字节码之后,加载之后通过解释器解析成汇编指令,
最终再转换成机器可以识别的机器指令。

JVM解释器是一种软件,我记得倒是原来有出现过直接执行Java 字节码的硬件。JVM解释器现在普遍是一种软件的话,那就可以将相同的一份Java字节码解释成不同的汇编指令,在通过硬件直接翻译成机器指令


Java代码编译

对于部分的热点代码,为提升Java的性能,JVM的解释器会直接将字节码编译为机器指令,并通过codecache缓存起来,codecache是属于堆外内存。如果系统的内存不够,codecache 存不下来了,JVM就不会即时编译了,这样Java的应用的执行速度会变慢,一般来说由于JVM的即时编译的存在,Java的运行速度初期会比较慢,后面会越来越快。

二. 代码的执行准备

代码编译成机器指令后,就需要执行了,执行需要一些环境,比如指令存在什么地方,cpu从什么位置取指令,如何取指令:


指令执行

图中:指令寄存器IP,指向待执行指令的地址;CPU的控制单元,将内存中的指令取出来,然后装载到指令寄存器IR中,这些加载来的指令还是二进制码串,需要通过指令译码器进行译码,如果需要取数据,则需要从内存中取数据,调用运算单元进行计算。

由于CPU和内存的速度相差太大,所以CPU不直接访问内存,而是通过多级缓存分级存取:


多级缓存

这里面的Cache缓存,又分为L1,L2,L3级别缓存,每个缓存的大小依次增大,访问速度越来越慢。如果CPU去取数据的时候,需要先判断主存地址是否在Cache中,如果在Cache中取数据给CPU,如果不在就从内存中取所在块的数据,然后在Cache中的一个空闲行中存入进去。

CPU如果去主存读的时候,并不是一次只拿一个值,而是根据数据访问的局部性原理,一般来说一个值被访问,这个值的物理地址周围的值同样可能被访问,所以一次存储一个缓存行,这样下次去取这个值的下一个值,比如数组的下一个元素的时候,直接从缓存中获取即可,而不用再从内存中取数据。
这样有好处,也会有坏处,比如如果这个缓存行中的一个数据发生了变动,那么整个缓存行就会失效,需要从新从内存取数据,所以一些高性能的程序需要解决这个问题,比如Disruptor高性能队列,就用如下的代码方式定义:

public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p8, p9, p10, p11, p12, p13, p14; // cache line padding

cursor 这个变量,无论怎么取,一个缓存行中,只会有一个cursor变量有效,就不会因为其他变量失效而影响这个变量了。

三. 代码执行

代码保存到内存中后,各种寄存器也准备到位,那么什么时候执行到我们的代码?CPU是周期性地从内存中取指令,译码指令,最终执行,周而复始。


CPU周期执行

为了支持多任务,CPU将执行时间这个资源划分成时间片,每个程序执行一段时间,或执行结束,或等待资源(比如等待IO),或时间片到了,CPU就会将正在执行的进程挂起,重新选择一个准备执行的进程进行执行。
CPU因为等待IO资源情况,也会主动放弃CPU,这时候会切换到另一任务执行。所以对于IO密集型的情况,多线程的效果要好,如果是CPU密集型的应用,多线程的效果要差的多。


等待IO主动放弃CPU

四. 内存分配

JVM启动的时候,就会在操作系统中产生一个进程,多个进程共享整个机器的物理内存。每个内存都有自己的虚拟内存空间。如果我们启动多个JVM进程打印一个Object对象的hashcode时候,会发现这些hashcode值一样的,这些hashcode值默认为Object对象的地址,也就是多个JVM进程返回Object对象地址是一样的,说明这些JVM进程都有相同的内存空间。

Object对象地址

每个Java进程都独占一个独立的虚拟内存空间,简化内存管理,而且可以防止进程对其他进程内存空间的破坏。

JVM虚拟内存

有了虚拟内存,最终肯定要申请物理内存,每个进程在申请内存的时候,操作系统负责物理内存和虚拟内存的映射,虚拟内存的地址可以超过物理内存地址,超出的就会发生数据的溢出,溢出的数据保存在磁盘上。

这些映射关系在操作系统中对应着页表:


页表

页表可以看成一个大数组,数组每一项对应一个内存页,每一项保存虚拟内存和物理内存或磁盘的页的映射关系。页表可以存在主存中,也可以存储在缓存区,我们将存储在高速缓存区的页表叫做TLAB,在linux中可以通过指令:

getconf PAGE_SIZE

来获取的内存的页面大小,一般为4096即 4KB。存在缓存TLAB中的页表访问显然更快,为了让TLAB存储更多页表数据,可以通过增大内存页的办法,像DPDK就要求设置大页内存,比如设置1G内存的大页。

五. JAVA代码的执行抽象

Java代码的执行的时候,无非是读取某个对象属性的值,然后通过一系列计算,将值再写入到某个属性中。我们获取一个对象之后,如何读取和设置它的属性那,可以参考Java中的反射的实现:


反射

首先,我们持有对象的引用后,就可以获取到这个对象的存储的虚拟地址,然后我们可以获取对象的属性相对于对象地址的偏移量,然后再查询页表,就获得存储属性的物理地址了,根据这个属性的类型,知道应该读取的内存的长度,就可以把这些属性读取出来了。写入也是如此。

属性相对于对象的偏移量在加载Class的时候就确定好了,它是和Class绑定的。如果一个对象只有一个属性,如果不压缩的话,对象头占128个字节,那么属性的偏移量就是128。
如果一个对象有多个属性,那么Java还会进行内存对齐,整个对象所占字节数对齐为8字节的倍数,这样做有两个好处:
1.提升内存访问性能。
2.保证一个属性的值只在一个缓存行中,如下图:


image.png

六. 总结

总结

我们首先要理解Java代码是怎么一步步变成机器指令的,理解其中的解释和即时编译。再次理解CPU的执行逻辑和分时间片调度;最后我们要理解进程的虚拟内存和物理内存是如何映射的,对象是如何访问属性的。

七. 说明

本文中的主要图片和文字整理自:https://time.geekbang.org/dailylesson/detail/100028498
文章。中间夹杂些我个人的理解。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容

  • 今天我放学了,我在客厅里发现三个大苹果。我把苹果分给爸爸妈妈,最后一个是我的,我真开心啊!
    韦宏烨阅读 285评论 0 2
  • 痴痴等待你的回复。
    庞灵灵阅读 159评论 0 0
  • 每一次走过这个地方,都有一个人拿着传单,很认真的对着每一个路过的人去聊健身。虽然很多人在拒绝,他依然在坚持,不知道...
    柒少年阅读 246评论 0 4
  • 窗户刚打开, 雨滴吹了进来, 醉人的风啊, 孕蕴着凉意, 我的思绪迟迟, 笔刚落下, 又得去伪装微笑。 把人生固守...
    牧文君阅读 336评论 2 9