从题目可以看出来,本文应该要介绍java程序的执行过程,但是一个java程序的执行过程必定涉及到java类成员的加载和执行,所有在本文中,先探讨java程序的执行过程,再探讨java类成员的加载执行过程。这是本文的整体脉络。

来源:https://zhuanlan.zhihu.com/p/78461807

什么是java程序?

在探究程序的执行过程之前,首先我们要搞清楚什么是程序?这个问题一直困扰了我很长一段时间,这段时间就是我的大学3年时间,我在大学里学习的专业不属于计算机专业,但是偏计算机专业,也可能由于我不是计算机专业,导致我没多少学习过计算机理论基础,所以程序这个名词一直困扰了我挺长的一段时间,但是在我即将踏入大四的前一个暑假,我对程序的概念有了自己的理解:程序就是一堆英文(因为编程语言最早由外国人发明),并且存储在一个文件中,我们称这个文件就是一个程序,我们可以根据不同语言来指定文件的扩展名,进而区分这个程序是那一门语言的程序。如:Java程序的扩展名为.java,JavaScript程序的扩展名为.js,C++程序的扩展名为.cpp等等。

综上可得:java程序就存储了一堆英文、扩展名为.java、保存在硬盘上的一个文件。

Java程序源代码的编写、编译和执行过程

Java程序的执行之前需要编写java源文件,并对这个源文件进行编译得到字节码文件,最后运行这个字节码文件,才能算是运行了一个Java程序。

源代码的编写:

可以使用记事本、IDE(Integrated Development Environment)来编写,记事本的唯一功能就是记录信息(源代码),而IDE本质上也是记事本,但是比记事本的功能更加强大,在我们编写代码时能给我们提供很多帮助,如提示代码编译不通过、关键字带特殊颜色并高亮显示、帮我们管理源文件和字节码文件等等一系列功能来协助我们开发者开发程序。

注意:建议初学者使用记事本来编写java程序,原因有以下几点:

  1. 熟悉java语法以及入口方法(main方法)标准格式的编写规则。
  2. 能让我们记住java源文件的编写规则(可以在一个源文件中编写多个类,但是只能有且只有一个public修饰的类,其余都只能是包访问权限修饰的类)。
  3. 能让我们记住java源文件的命名规则(源文件名必须和public修饰的类名一致)。
  4. 能让我们练指法,敲代码的速度能更快。

源代码的编译和执行:

在介绍源代码的编译之前,先介绍java开发工具,java开发工具在java安装目录下的bin文件夹中,bin是binary的缩写,是二进制的意思,bin文件夹下的所有文件所有可执行文件,用于对java程序进行各种操作,如编译、运行、反编译、生成注释文档等等。

源文件的编译需要用到javac.exe工具,编译之后得到一个字节码文件,执行java源文件就是执行这个字节码文件,执行字节码文件需要用到java.exe工具。所以一个java源文件从编写、编译、执行全过程如下图:

注意:字节码文件的具体执行过程由JVM来管理,这个执行过程在后续会有说明。

字节码文件的加载过程

以上的篇幅都是从宏观的角度来探讨java程序的执行过程,在本节中将从微观的角度来探讨java程序的执行过程。我们需要清楚的一点是,我们所有程序想要运行的话,都要拿到内存上运行,我们电脑上的资源管理器中的进程就是目前咱们电脑在内存中运行的程序。

如下图:

其中应用是我们用户手动打开的程序,而后台程序是操作系统启动时自动打开的程序,用于支持操作系统的正常运行。

所以说如果想要将java源文件真正运行起来,需要将java源文件(.java)放在内存中跑起来,但是windows平台下,通常只能运行.exe文件,所以java源文件显然是不能直接运行在windows平台下的内存中,我们发现源文件经过编译得到的字节码文件(.class)也不是.exe文件。那么我们的java源文件怎么才能放置到内存中运行呢?这时候JVM就要闪亮登场了,首先我们需要明白的一点就是Java是一门跨平台(操作系统)语言,所以java源文件不像C源文件、C++源文件等等不跨平台的语言那样经过编译直接得到一个当前操作系统的可执行文件。否则java的口号“一次编译,到处运行”不就是水我们麽!所以注定了java源文件经过编译后绝不可能得到一个平台可执行文件。当我们需要执行编译好的字节码文件时,需要借助jvm代理操作系统来执行字节码文件,所以我们经常说jvm是一台模拟操作系统的一个模拟计算机。

既然jvm可以执行字节码文件,那么我们就要来聊聊jvm的工作原理了,当我们通过jvm执行一个java字节码文件时,jvm首先在内存中开辟一块jvm内存,专门用于执行java字节码文件,另外jvm会通过一个名为类加载器(Class Loader)这么一个角色将电脑硬盘上字节码文件加载到jvm内存中,然后在jvm内存中执行字节码文件中记录的信息(程序)。此时我们编写的java源文件才真正的在内存中运行起来。

注意:jvm内存就是我们经常说的栈内存、堆内存、方法区(常量池、方法区、静态元素区)的一个总称。

如下图:

现在我们知道了在我们通过java.exe执行工具执行字节码文件时,jvm会自动的将这个字节码文件通过类加载器Class Loader将字节码文件加载到jvm内存空间中的方法区中,那么这个字节码文件是原封不动的从硬盘搬到内存中吗?字节码文件中属性和方法是怎么个加载过程?这些问题我们继续探讨。

类成员的加载和执行过程

字节码文件中包含的信息有:类名、类成员(属性、方法)。那么研究字节码文件的执行过程无非就是研究字节码文件中的类成员的加载和执行过程。我们知道一个类的类成员大致可以分为属性和方法,但是细分可以分为一般属性、一般方法、构造代码块、构造方法、静态属性、静态方法、静态代码块,那么接下来的探讨可以分为三个部分:

  1. 属性和方法的加载位置。
  2. 属性和方法的加载顺序。
  3. 构造方法、构造代码块、静态代码块的执行顺序。

属性和方法加载位置:

一般属性、一般方法、构造代码块、构造方法加载到方法区中,而静态属性、静态方法、静态代码块则存储在方法区中静态元素区中。如下图:

属性和方法的加载顺序:

依次加载:一般属性、一般方法、构造代码块、构造方法、静态元素、静态方法、静态代码块。

如下图:

当我们通过new的方式创建一个Person对象时,就会在堆内存中开辟一块内存空间,将Person类模板上的一般属性加载到这块内存上,并新增了一个this属性用于指向这块内存自己本身,此外这块内存空间保存着其对应的类。如下图:

注意:这个图还存在一些瑕疵,因为这个内存图没有暂时还没有考虑到Person类的父类的加载过程。

构造方法、构造代码块、静态代码块的执行顺序:

当我们通过new的方式创建一个Person对象时,需要调用Person类的构造方法,在调用构造方法之前,我们需要将Person.class字节码文件加载到jvm内存中的方法区中,此时我们才能找到Person类的构造方法并调用,但是需要注意的是,此时Person中的属性和方法都必须加载到方法区中才能访问,而静态代码块一旦加载完毕之后就会立即执行,所以在调用构造方法之前,静态代码块就已经执行一次且仅一次。当静态代码块执行完毕之后,我们才能调用构造方法,而在调用构造方法之前,构造代码块需要执行一遍。

综上:静态代码块先于构造代码块执行,构造代码块先于构造方法执行。

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Person {

private String field = "我是person中的一般属性";
private static String staticField = "我是person中的静态属性";

public Person(){
super();
System.out.println("我是person中的默认无参数构造方法");
}

public void method() {
System.out.println("我是person中的一般方法");
}

public static void staticMethod(){
System.out.println("我是person中的静态方法");
}

{
System.out.println(Person.staticField);
Person.staticMethod();
System.out.println(this.field);
this.method();
System.out.println("我是person中的构造代码块");
}

static{
System.out.println(Person.staticField);
Person.staticMethod();
System.out.println("我是person中的静态代码块");
}
}
public class TestMain {

public static void main(String[] args) {

Person p = new Person();
}
}

测试结果:

1
2
3
4
5
6
7
8
9
我是person中的静态属性
我是person中的静态方法
我是person中的静态代码块
我是person中的静态属性
我是person中的静态方法
我是person中的一般属性
我是person中的一般方法
我是person中的构造代码块
我是person中的默认无参数构造方法

打印结果解读:

当我们通过new的方式创建Person对象时,会调用Person类的构造方法,通过上面的分析我们可以知道,静态代码块先于构造代码块执行,构造代码块先于构造方法执行。而类成员的加载顺序:一般属性、一般方法、构造代码块、构造方法、静态属性、静态方法、静态代码块,所以静态代码块执行的时候可以访问到静态元素和静态方法,而构造代码块执行的时候可以访问到静态属性、静态方法、一般属性、一般方法,最后是构造方法的执行。

注意:静态元素不能访问非静态元素(一对多,不明确访问哪个对象的属性或者方法),而非静态元素可以访问静态元素(多对一,静态元素有且只有一份,明确访问)。

综上可得到类对象的创建过程(未考虑父类的情况):

  1. 加载类的一般属性、一般方法、构造代码块、构造方法。
  2. 开辟一块静态元素空间,加载类的静态属性、静态方法、静态代码块。
  3. 执行类的静态代码块。
  4. 在堆内存中开辟一块空间,用于表示该类的对象。
  5. 在该对象空间内加载类的一般属性,this属性,持有类的信息。
  6. 执行对象的构造代码块。
  7. 执行对象的构造方法。
  8. 复制一份对象空间中的this属性值给栈内存中的变量。

存在继承关系的类成员加载执行过程

上文我们忽略当前类的父类的加载过程,接下来我们来探讨一下父类的加载过程对当前类的加载过程的影响。

引入了父类的加载过程,无非就是在在加载子类属性或者方法之前先加载父类的属性和方法,这好比先有父亲再有儿子的关系,其实编程的思维和我们现实生活中的考虑问题的思维很像。

存在继承关系的子类对象创建过程:

  1. 加载父类的一般属性、一般方法、构造代码块、构造方法。
  2. 开辟一块属于父类的静态元素空间,加载静态属性、静态方法、静态代码块。
  3. 执行父类的静态代码块。
  4. 加载子类的一般属性、一般方法、构造代码块、构造方法。
  5. 开辟一块属于子类的静态元素空间,加载静态属性、静态方法、静态代码块。
  6. 执行子类的静态代码块。
  7. 在堆内存中开辟一块子类对象空间。
  8. 在子类对象空间中开辟一块名为super的父类对象空间,并在这块空间中加载父类的一般属性、持有类的信息。
  9. 执行父类的构造代码块。
  10. 执行父类的构造方法。
  11. 在子类对象空间中加载子类的一般属性、this属性、持有类的信息。
  12. 执行子类的构造代码块。
  13. 执行子类的构造方法。
  14. 将子类对象空间中的this属性值复制一份给栈内存中的子类类型变量。

过程图如下:

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Person {

private String field = "我是person中的一般属性";
private static String staticField = "我是person中的静态属性";

public Person(){
super();
System.out.println("我是person中的默认无参数构造方法");
}

public void studentMethod() {
System.out.println("我是person中的一般方法");
}

public static void staticMethod(){
System.out.println("我是person中的静态方法");
}

{
System.out.println(Person.staticField);
Person.staticMethod();
System.out.println(this.field);
this.studentMethod();
System.out.println("我是person中的构造代码块");
}

static{
System.out.println(Person.staticField);
Person.staticMethod();
System.out.println("我是person中的静态代码块");
}
}
public class Student extends Person {

private String field = "我是student中的一般属性";
private static String staticField = "我是student中的静态属性";

public Student() {
super();
System.out.println("我是student中的默认无参数构造方法");
}

public void method() {
System.out.println("我是student中的一般方法");
}

public static void staticMethod() {
System.out.println("我是student中的静态方法");
}

{
System.out.println(Student.staticField);
Student.staticMethod();
System.out.println(this.field);
this.method();
System.out.println("我是student中的构造代码块");
}

static{
System.out.println(Student.staticField);
Student.staticMethod();
System.out.println("我是student中的静态代码块");
}
}
public class TestMain {

public static void main(String[] args) {

Student s = new Student();
}
}

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我是person中的静态属性
我是person中的静态方法
我是person中的静态代码块
我是student中的静态属性
我是student中的静态方法
我是student中的静态代码块
我是person中的静态属性
我是person中的静态方法
我是person中的一般属性
我是person中的一般方法
我是person中的构造代码块
我是person中的默认无参数构造方法
我是student中的静态属性
我是student中的静态方法
我是student中的一般属性
我是student中的一般方法
我是student中的构造代码块
我是student中的默认无参数构造方法

以上就是本人对java程序执行过程的理解和剖析,欢迎指点。