当前位置:主页 > 查看内容

没想到进入Main函数前,发生了这么多事!

发布时间:2021-04-23 00:00| 位朋友查看

简介:题外话 最近这段时间,轩辕有些迷茫了,工作生活中一堆事儿,忙得我两头摸黑,很难找到时间静下心来写文章,就连你现在看到的这一篇还是我点灯熬油到1点钟才写完的。 估计朋友们也有发现了,这段时间故事文章少了很多,确实是这样,不像水文几下完事儿,也不……

题外话

最近这段时间,轩辕有些迷茫了,工作生活中一堆事儿,忙得我两头摸黑,很难找到时间静下心来写文章,就连你现在看到的这一篇还是我点灯熬油到1点钟才写完的。

估计朋友们也有发现了,这段时间故事文章少了很多,确实是这样,不像水文几下完事儿,也不像普通技术文章按照标准流程走就行。故事文需要放空自己,有好的灵感才能一气呵成。

这段时间有好几个选题,都开了个头,占了个坑,然后因为没时间思考,所以一直留着···

所以今天,先写了一篇别的顶一顶,故事文章大家再给我点时间吧,感谢各位老铁的支持!

在之前的一篇文章中,聊过从创建进程到进入main函数,发生了什么?

但当时只是针对C/C++这样的native语言,从操作系统(Linux & Windows)的层面去探讨了程序的启动过程,而对Java、Python这样的基于虚拟机/解释器的语言并未提及。

今天,咱们就一起来探索下在Java语言中,你写的main方法又是怎么被执行到的?

对于Java而言,其底层是Java虚拟机在跑着,也就是JVM,这篇文章如无特殊说明,默认以Hotspot为研究对象。

先来回顾一下那篇文章,对于C/C++程序而言,从创建进程到进入main函数,主要就是经历了四个阶段:

  • 进程 & 主线程创建阶段
  • 主线程启动执行并进行进程级初始化操作(如加载系统动态链接库)
  • 主线程进入可执行文件的入口(OEP)并进行C/C++运行时库初始化
  • 从C/C++运行时库调入main函数

你知道的,Java的虚拟机JVM主要是C++编写的,所以JVM本质上也算是一个C++程序。

因此,上面的四个阶段,对于JVM而言,同样适用。

只不过呢,对于C/C++程序而言,到这里就已经进入main函数了,话题就可以结束了,而对于Java程序,执行到JVM的main,一切才刚刚开始。

JVM的main

故事,要从JVM的main函数开始讲起···

你应该知道的,不管你是普通Java程序,还是用的Spring或者其他什么框架,最终的程序都是在一个Java进程中运行的,这个进程的可执行文件就是一个exe(windows上)或者elf(linux上)。

咱们就从这个可执行文件入手,以Linux系统上的Java8版本为例,用反汇编神器IDA打开可以看到,这个可执行文件的入口:

和咱们在上一篇分析的流程符合,进入这个程序启动入口后,会经过一系列的调用,最后来到main函数:

反汇编看着好头大,好在,HotSpot虚拟机有开源版本,咱们可以去OpenJDK中找来这个main函数的源码瞧瞧。

不同版本差异还是挺大,这里以Java8为例:

代码路径:https://github.com/openjdk/jdk/blob/jdk8-b20/jdk/src/share/bin/main.c

在这个代码中除了main函数,还可以看到如果定义了JAVAW宏定义,则入口从main变成了WinMain函数,做过Windows应用程序开发的朋友这个时候应该露出了满意的微笑。

如果定义了JAVAW,则是一个Win32 GUI的程序,当然在Linux上是肯定没有这个宏定义的,不过这不是本文的主题。

可以看到main函数只是一个包装,直接就进入了JLI_Launch中。

这个函数位于同目录下的隔壁java.c文件中,是JVM非常重要的初始化函数,主要完成了下面几件事情:

  • 参数解析,环境配置
  • 检查Java运行环境
  • 加载JVM核心动态库libjvm.so
  • 创建并初始化Java虚拟机对象

这些过程都不是我们这篇文章探究的目标,咱们继续把目光聚焦在Java中的main函数是怎么得到调用的。

在JLI_Launch的结尾,调用了ContinueInNewThread,从这个函数的名字我们也能窥探它的作用。

这个函数还是一层封装,内部调用了真正干活的函数ContinueInNewThread0:

接下来就是创建线程来继续后面的事情了,不过创建线程涉及到操作系统API的调用,所以这个函数在不同版本的系统中都有对应的实现。来看传给它的第一个参数,这是新线程启动后将要执行的入口函数:JavaMain。

JavaMain

这个函数的名字就有点意思了,看起来,快要进入Java的地界儿了,加油继续看下去:

  1. int JNICALL  JavaMain(void * _args) { 
  2.   
  3.     // ... 
  4.     // 寻找启动类 
  5.     mainClass = LoadMainClass(env, mode, what); 
  6.     // ... 
  7.     // 寻找启动类中的main函数 
  8.     mainID = (*env)->GetStaticMethodID(env, mainClass, "main"
  9.                                        "([Ljava/lang/String;)V"); 
  10.     // ... 
  11.     // 调用它 
  12.     (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); 
  13.     // ... 

JavaMain中的细节挺多的,咱们抽出需要关心的,要调用咱们写的main方法就像把大象关进冰箱一共三步:

  • 找到启动类
  • 找到启动类中的main方法
  • 调用它

具体寻找的过程这里就不展开了,有些繁琐,但你应该能猜到,Java代码编译后都是以class文件的形式存储的,所以这个寻找的背后少不了要涉及到class类加载等一系列的工作。

总之,一顿操作猛如虎,嘿,JVM把咱们写的main方法找到了!接下来就是调用它了。

进入Java世界

调用main方法的是CallStaticVoidMethod,从名字可以看到,这是在调用一个静态的、返回值为空的方法。注意了,C++的地盘快到边境了,咱们即将通过它来到美丽的Java新世界!

这个函数内部后面会来到:

  1. JavaCalls::call(result, method, &java_args, CHECK); 

最终,会创建Java方法栈帧,准备好模板解释器,随后转向解释器入口开始执行字节码,正式进入Java世界!

进入Java世界第一站,就是前面找到的启动类的main方法,在这里开启程序在Java世界的征程。

总结

现在可以来回答这个问题了:从创建进程到Java的main方法,经历了什么?

咱们来划分三个大的阶段:

第一阶段:操作系统层面进程和主线程的创建

第二阶段:主线程启动执行并进入到Java可执行文件(exe/elf)中的main函数(C++层面)

第三阶段:创建JVM,寻找启动类中的main方法,启动解释器执行对应字节码进入Java世界

本文转载自微信公众号「编程技术宇宙」,作者轩辕之风 。转载本文请联系 编程技术宇宙公众号


本文转载自网络,原文链接:https://mp.weixin.qq.com/s/zeQJAPqh6dsUDeXsi2U8mA
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐