写在前面
最近一个多月面试了很多java
求职者,资深的高级的初级的都有,我都会问一个问题:我们学java第一天写的hello world 是怎么被运行起来的
几乎是全军覆没,没有一个能回答到40%
的。
所以,我抽时间写了这篇文章,当然这也是在我的理解和认知水平上对这个问题的回答,不对之处,敬请斧正。
程序的几个关键时期简略介绍
-
编码期:这个阶段通常对应我们在开发工具(如
idea
)中编写代码。 -
编译期:这个阶段是使用
jdk
提供的javac
工具对我们写的代码按照jvm
发行规范的标准进行编译。请注意,这里可能发生第一次指令重排,这个发生的原因是编译器的初次解析,为了应对后面的运行工作更加有效。 -
装载期:这个阶段是把编译期的文件进行基础装载进内存,请注意,并不是所有的
.class
文件都会被装载,这里是按需装载。这里会有一个双亲委派策略来保证装载安全性、效率和唯一性。 -
验证期:这个阶段是
jvm
按照当前的jvm
运行规范来验证文件是否合法,这里会重点验证在编译期产生的.class
魔数及相关jvm
标准定义信息。 -
初始化期:这个阶段是指即将运行
.class
,这里会在jvm
堆上分配一块默认大小(这个大小受jvm
版本自行调整,也可以根据参数手动指定大小)的区域用来存储实际的.class实例
信息,这里存储的是该.class
的字节码、装载器、相应的方法属性指针链表等信息。 -
方法运行期:这个阶段其实分为3个阶段;
-
方法运行前:开始运行方法前的准备工作,向
jvm
申请一块默认大小的内存(这个大小受jvm
版本自行调整,也可以根据参数手动指定大小),标记为私有区域,并且会根据当前方法的信息,如方法指令,参数等往公共堆中寻找对应的指针链表存到这个区域内,并且这块内存数据的存取方式是一个栈,也就是常说的方法栈。在这个过程中存在的都是指令及内存指针。另外这里会有一个私有的程序计数器,请注意这个程序计数器是对前.class实例
而言的,它是由一个线程创建的(你可以理解成一个线程对应一个私有的程序计数器)。请注意,在方法栈信息装入具体的运行指令的时候,由于受jvm
的优化机制和程序计数器影响,可能会发生第二次指令重排。 -
方法运行中:由程序计数器根据指令做一些调度动作,对应的就是在方法栈中,一个个指令入栈出栈。请注意,在这个阶段,可能会发生第三次指令重排,与前
2
次不同的是,这次是不是jvm
的行为,而是cpu
自身的指令优化机制。除此之外,这里有一个新的概念产生:内存屏障
,我就不展开说了,这个不在本文的陈述范围。 -
方法运行后:方法是否执行完毕有几个标准:
- 是否遇到了
to
、return
、throw(这个得没有finally的情况下)
- 计数器完成了计数,没有新的指令了
最简单的情况下方法运行完成后会开始销毁方法栈,销毁程序计数器。
这里有另外一种情况:A方法 return B方法
,这个时候,当B方法
开始在方法运行(前)期时,会将A方法
的方法栈保留,并指向B方法
的方法栈,包括某些可传递结果。扩展引申一下,这就是为什么递归或者方法链路太深会导致发生栈溢出的几率变高的原因。 - 是否遇到了
-
-
资源清理期:运行完成后会自动销毁对应的方法栈,会在堆上标记相应的
.class实例
,这个标记是有前置条件的,通过gc
的可达性算法做引用判定。
对指令重排
的补充,指令重排
遵循一个特定原则,这个原则百度一下你就知道,我不赘述,但是对于cpu
而言,除了遵循此原则外,还有可能发生指令替换
。
这个角度回答的很笼统,在高铁上无聊随手写写,对其内容没有过多考虑。将就着看看吧,写文章时间过得快,我快到站了,不写了。下次有时间从
.class文件的结构
、
类装载的详细过程
、
加载机制
、
jvm内存模型及自动分配内存
等角度来拆解这个过程。
要是等不及的,可以看看下面的链接,一张图解决你所有的困惑 (https://github.com/liuchengts/mind-map)[https://github.com/liuchengts/mind-map]
评论区