Java执行前都干了什么?

Java第一个HelloWorld程序,控制台显示出HelloWorld之前都做了什么?越详细越好。

public class Test{
    public static void main(String[] args){
        System.out.println('HelloWorld!');
    }
}

前言

打开Windows电脑(已经安装并配置好JDK),新建一个记事本,输入如上代码,保存为Test.java(刚学习java的时候不都是喜欢用Test命名吗?Test01、Test02的)。打开cmd,编译执行后,cmd中输出“HelloWorld!”倒吸一口凉气,这TM啥玩意?

作为一个Java工程师,在接触到Java后总要回过头来探究这第一个程序。输出前都做了什么?(其实我都不想告诉你们这个是我遇到的面试题,崩溃了)。

以下就是简单的分析过程。(如有不详细的地方请查看文章最后的后序部分的参考引用文章,如果有错误的地方,请提出。本人邮箱:584841780@qq.com)

1、windows为什么可以运行java

1.1path说明

我们都知道在CMD中运行某些可执行文件(exe)或者批处理文件(bat),一般都是需要先“cd 路径”进入该文件所在的目录,或者是使用“绝对路径+\可执行文件或者批处理文件名”的方式进行运行。如果不想这样做呢?那就需要配置path环境变量。

Windows在执行用户命令时,用户若没给出绝对路径,则首先在当前目录下查找是否存在目标文件;如果找不到,则依次根据path路径中的路径寻找目标文件。系统按照第一次找到目标为准,如果都为找到,则会出现“ 'XX’不是内部命令或者外部命令,也不是可执行的程序”。

1.2配置JDK环境变量

配置JDK环境变量JAVA_HOME、CLASSPATH时,JAVA_HOME指向JDK安装目录,CLASSPATH指向lib、rt、tools进行加载指定的库类。path中指向运行的二进制文件夹bin中,使里面的类似java、javac、javap等都是可以在windows中直接使用。这也是使用JDK运行java之前为什么要进行JDK环境变量配置的原因。

2、编译

使用javac进行编译(javac源码请点击),生成.class二进制文件。以下是javac编译过程源码,汉字部分是人为添加注释。

/**as a JavaCompiler can only be used once ,throw an exception if it has been used before.
*/
//判断是否已经编译过
if(hasBeenUsed)
    throw new AssertionError('attempt to reuse JavaCompiler');
hasBeenUsed = true ;

start_msec = now();
try{
    //准备过程,初始化插入式注解处理器
    initProcessAnnotations(processors);
    //These method calls must be chained to avoid memory leaks
    delegateCompiler = processAnnotations(//过程2:执行注解处理
        enterTrees(stopIfError(CompileState.PARSE,//过程1.2:输入到符号表
        parseFiles(sourceFileObjects))),classnames);//过程1.1:分析词法、语法
    //过程3:分析语义以及生成字节码
    delegateCompiler.compile2();
    //完成编译
    delegateCompiler.close();
}

2.1分析和输入到符号表

Parse and Enter。

2.1.1词法分析

com.sun.tools.javac.parser.Scanner类实现。源代码的字符流转变成标记(token)集合。单个字符是编写程序的最小单位,标记是编译过程的最小单位。关键字、运算符、字面量、变量名都可看成标记。

2.1.2语法分析

com.sun.tools.javac.parser.Parser类实现 。是根据Token序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释都可以是一个语法结构。抽象语法树由com.sun.tools.javac.tree.JCTree类表示

2.1.3填充符号表

详情请参考上一个链接。符号表就暂时看成map的k-v即可,在之后生成的class文件中都是这种k-v结构。com.sun.tools.javac.comp.Enter类实现 。.java文件类中如果没有构造器,这时候会默认生成无参构造器,访问权限和类一致(和“生成class文件”时生成构造器不是同一回事)。

2.2注解处理

Annotation Processing。参照上图源码,插入式注解处理器的初始化过程是在initProcessAnnotations()方法中完成的,而它的执行过程则是在processAnnotations()方法中完成的。

2.3语义分析和生成class文件

Analyse and Generate。进行源代码是符号逻辑的审查。

2.3.1标记检查

在这里语义分析,类似如下会惊醒语法折叠,把sum的值在语法树中标记成11;

int sum = 10 + 1 ;

其他的还有类似变量类型赋值是否正确,是否应该强转。实现类是com.sun.tools.javac.comp.Attr类和com.sun.tools.javac.comp.Check类 。

2.3.2数据及控制流分析

检测局部变量使用前是否有赋值、方法是否有正确返回值、受查异常是否进行正确处理。

这里有个问题。就是昨天说的标记问题flag????2018年12月5日01:22:03

2.3.3解语法糖

泛型、变长参数、自动装箱/拆箱等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。

desugar()触发,com.sun.tools.javac.comp.TransTypes类和com.sun.tools.javac.comp.Lower类实现。

2.3.4生成的class文件

com.sun.tools.javac.jvm.Gen类实现。将语法树和符号表转成字节码

实例构造器()方法和类构造器()方法就是在这个阶段添加到语法树之中的(注意,这里的实例构造器并不是指默认构造函数,如果用户代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性(public、protected、private)与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成),这两个构造器的产生过程实际上是一个代码收敛的过程,编译器会把语句块(对于实例构造器而言是“{}”块,对于类构造器而言是“static{}”块)、变量初始化(实例变量和类变量)、调用父类的实例构造器(仅仅是实例构造器,()方法中无须调用父类的()方法,虚拟机会自动保证父类构造器的执行,但在()方法中经常会生成调用java.lang.Object的()方法的代码)等操作收敛到()和方法之中,并且保证一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行,上面所述的动作由Gen.normalizeDefs()方法来实现。

其次,类似如下代码会进行转换

String filePathAndName = '';
String filePath = 'D:/java';
String fileName = 'test.java';
filePathAndName = filePath+File.separator+fileName ;
/**源码中String是final的并且是不可以修改的,这里的上一行代码在生成字节码的时候回默认变成以下StringBuffer.append()进行操作*/

反编译如下图,可以看出String更改为StringBuffer或者StringBuilder的append()



com.tools.javac.jvm.ClassWriter的writeClass()方法输出字节码

文件具体内容是以“cafebabe”魔数开头的二进制文件(4个字节),虚拟机根据魔数判断是否为.class文件。5-6次版本号,7-8主版本号。版本号对应关系如下:

JDK版本号 Class版本号 16进制
1.1 45.0 00 00 00 2D
1.2 46.0 00 00 00 2E
1.3 47.0 00 00 00 2F
1.4 48.0 00 00 00 30
1.5 49.0 00 00 00 31
1.6 50.0 00 00 00 32
1.7 51.0 00 00 00 33
1.8 52.0 00 00 00 34

剩下的内容是类似k-v。详细情况请参考最下方参考文献

3、加载

就是把class文件读入内存,创建一个java.lang.Class的实例对象,因为所有的类其实都是一种Class的对象。

3.1加载器种类

Bootstrap ClassLoader 根加载器:JVM自身实现,负责加载java核心类。例如:String、System。核心库都在%JAVA_HOME%/jre/lib/rt.jar中(在eclipse中看不到源码添加rt.jar即可)。

Extension ClassLoader扩展加载器:主要为了java扩展新功能,扩展包在%JAVA_HOME%/jre/lib/ext中

System ClassLoader (APP ClassLoader)系统类加载器:加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量指定的jar包和类路径。(一般情况下自定义的类加载器都是以此为父类加载器)

Custom ClassLoader 用户自定义加载器:继承ClassLoader,覆写findClass()。

3.2加载机制

全盘负责:加载器加载某Class则需要加载其依赖的其他Class,除非限时使用另一个加载器载入。

父类委托:让父类加载器加载Class,若无法加载则自己加载。(个人理解类似初始化总要初始化父类)

缓存:所有加载过的Class都会被放在缓存区,进行加载Class时先到缓存区寻找是否存在该Class,如果不存在则读取赌赢的二进制数据,转成Class放在缓存区。正因为缓存机制,每次修改Class后需要重启JVM。例如:更新服务器上的项目,不管使用增量包还是全量包都要重启项目。最好在更新前停止项目,更新后重启。(一些特定的xml,html等之类的不需要重启,也是需要看情况的)。

另外,ClassLoader类中的findLoadedClass(String name)即缓存机制的体现

其实就是一种双亲委派机制

4、连接

把类的二进制数据合并到JRE中。

4.1验证

检测被加载的是否有正确的内部结构,并和其他类协调一致。

4.2准备

为类的Field分配内存,并设置默认初始值。

4.3解析

将类的二进制数据中的符号引用替换成直接引用。

5、初始化

JVM负责对静态Field进行初始化:

方式1:声明静态Field时指定初始值。

方式2:使用静态初始化块指定初始值。例如:

static{
    a = 5;//方式2
}
static a = 9 ;//方式1

所以每次最先初始化的总是java.lang.Object类。

初始化的时机:

​ 1、创建类实例:使用new;使用反射;使用反序列化。

Map<String, String> hashMap =new HashMap();//使用new
Class clazz = Class.forName(“com.mysql.jdbc.Driver”);//使用反射

public Class Person implements Serializable{ //实现Serializable序列化接口
    private static final long serialVersionUID = 1L;//定义serialVersionUID序列化ID
    //省略Person的各种属性和setter、getter方法
}
//最后使用I/O流ObjectInputStream的readObject()方法,使文件流反序列化成对象

​ 2、调用类的静态方法。

​ 3、访问类或者接口的静态Field或者为静态Field赋值。

​ 4、初始化某个类的子类(原因参考上面流程图)。

​ 5、调用java.exe。

注意:“宏变量”的调用不会初始化其所在类(static final的Field在编译期间就确定下来),例如:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
后序:

引用部分来自以下文章

原文:《Javac编译器详解》 https://blog.csdn.net/tanga842428/article/details/52384127

原文: 《Java Class 文件详解》 https://www.cnblogs.com/yaoyinglong/p/4297793.html

原文:《疯狂java讲义》 http://www.bubuko.com/infodetail-1803036.html

(0)

相关推荐