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