JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇
作为一个使用Java语言开发的程序员,我们都知道,要想运行Java程序至少需要安装JRE(安装JDK也没问题)。我们也知道我们Java程序员编写的程序代码文件是*.java的,而JRE运行的是*.class的文件。所以,我们需要将java文件编译成class文件然后才可以。那么,你有没有想过,一个java文件是怎么运行起来的呢?中间都经历了哪些环节呢?我们都知道JVM是Java虚拟机,那么,有没有思考过JVM的内存模型是什么呢?我们new出来的对象,声明不同类型的变量又是存放在JVM哪个位置呢?
本文是凯哥(凯哥Java:kaigejava)学习JVM系列教程第一篇。欢迎大家一起学习
本文目标:
通过本文学习后,希望大家对JVM类加载过程有个了解。
上面程序很简单。那么,有没有想过上面代码怎么运行的呢?
选中main方法,然后ruan as...,编译后,运行输出。这个流程我想大家都很熟悉的。那么对应的流程应该是什么样的呢?如下图:
在Run的时候,先将.java文件编译成.class文件。然后,在通过类加载器,将class文件加载到JVM中,然后在运行。输出结果。
那么为什么编译好的AppTest.class可以加载到JVM中呢?可以被JVM识别呢?
一个java类的一生都会经历哪些步骤呢?
如下图:
在我们run的时候,AppTest.java类先经过编译后,编译成了AppTest.class文件。JVM把class文件加载到内存后需要经历:加载-验证-准备-解析-初始化-使用-卸载这七个阶段。
第一个问题:JVM在什么时候会加载一个类呢?起始也就是在什么时候会加载.class字节码文件到JVM的内存中去呢?上面我们写的,当我们run的时候,才执行的。所以答案就很明确了,就是在你代码中需要使用到这个类的时候,就去加载的。
具体每一步:
加载
加载阶段是将class文件从磁盘或者jar等读到JVM内存中,并为其创建一个Class对象。任何一个类被使用时候系统都会为其创建一个Class对象的。
加载的同时将加载的这些数据转换成方法区中运行时数据(运行时候数据区:静态变量、静态代码块、常量池等),作为方法区数据的访问入口
这个很好理解的。我要想使用你,需要先得到你,是不是。结合上面我们自己写的AppTest类。在此阶段应该是:
扩展:
在类加载阶段JVM都做了什么?获取class文件方式都有哪些?
1.1:在类加载的时候JVM完成了以下:
- 根据类的全路径(全限定名)来获取到该类的二进制字节流
(我们知道,在电脑的世界中,什么都是二进制形式存在的)
- 将加载的字节流中所代表的静态存储结构转换成方法区运行时数据结构
(这个话具体怎么理解,有哪位能留言教教凯哥)
- 将加载的对象在内存中生成一个代表了该类的jvaa.lang.Class对象。这个Class对象作为加载进来对象在方法区各种数据的访问入口。
(要想在内存中访问AppTest这个字节码类中的属性或者方法的时候,可以在内存中方法区找到对应的Class对象。这个Class就是入口)
关于方法区在后面文章中,凯哥会详细讲讲。
1.2:获取class文件的方式
- 可以直接从本地的磁盘文件获取
- 可以从忘了下载class文件
- 可以从ZIP或者jar等文件中
- Java源文件动态编译的class文件
在一个类运行生命周期内,类加载(加载获取类的二进制字节流)阶段,是可控性最强的阶段。因为在这个阶段,我们程序员可以使用系统提供的类加载去来加载完成,也可以使用自己自定义的类加载来完成.(类加载器在后面文章详细讲讲)
1.3:类加载的具体时机,在文章最后,凯哥会列出来。
验证
将上一步加载到内存中的Class对象进行校验。确保加载的类的信息符合JVM的规范。确保没有安全方面的问题。
这个很好理解了,我要使用你,得到你好,我要检查你是不是符合标准的。如果不合法,就没法使用。
在此阶段如下图:
扩展:验证都验证哪些方面?
- 文件给是验证:验证加载的字节流是否 符合Class文件格式的规范。
例如:是否已咖啡babe开头(0xCAFEBABE),主次版八号是否在当前JVM的处理范围内等等
比如你在JDK1.8下编译的class文件,放到JDK1.6版本的JVM中,有可能就运行不了的
- 元数据验证:对字节码描述的信息进行语义分析。保证描述信息符合Java语言规范。
例如:这个类如果有父类,是否实现了父类的抽象方法等.
- 字节码验证
- 符号引用验证:确保解析动作是正确的。
例如:通过符号引用能找到对应点的类和方法。比如com.kaigejava.Person.getAge()
在比如:符号引用中类、属性、方法的访问性是否能被当前类访问等等。
准备
准备阶段,就是给加载进来且验证通过的Class类分配空间的。这里是给类里面的变量(也就是static修饰的变量)分配空间的,同时给变量一个默认的初始值。
如下图:
在准备阶段时候static int m 被分配了4个字节的空间,且分配了默认初始值为0(注意默认初始值是0).
PS:int类型占用4个字节。int的默认值是0.如果是对象的话。默认为null
在此阶段AppTest.class如下图:
该阶段需要注意:
- 在此阶段值只对static修饰的静态变量进行内存分配,赋默认值的(比如0、0L、0D、null、false等);
- 对于final修饰的静态字面值常量直接赋初始值(注意:这里的初始值并不是默认值。如果不是字面值静态常量,那么会和静态变量一样赋默认值)
比如:final int x = 1;这个在此阶段就给赋值的就是1而不是0
解析
解析是将常量池中的符号引用替换为直接引用(内存地址)的过程。
在此阶段AppTest类如下图:
扩展:
符号引用:
就是一组符号来描述目标的。可以是任何字面量。这个属于编译原理方面的东西。
比如:可以是一个类的完整类名字(com.kaigejava.Person)、字段的名称和描述符、方法的名称和描述等。
直接引用:
就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。比如指向方法区中某一个类的一个指针。
例如:在AppTest这个类中,有个static的静态变量p。这个静态变量p又是一个自定义的类型(com.kaigejava.Person),那么在经过解析阶段后,这个静态的p变量将是一个指针(比如0xddff1),这个指针指向该类在方法区的内存地址值。具体见凯哥后续文章,将会详细讲解。
初始化
到了此阶段(初始化阶段),JVM才开始真正的执行类中定义的Java代码。
当进行到初始化阶段的时候,就是执行类的构造器<clinit>()方法的过程。
- <clinit>()方法是由编译器自动收集类中的所有类变量赋值动作和静态语句。
- <clinit>()方法与类的构造器不同。此方法不需要显示的调用类的父构造器(如果类有父类的话),虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此JVM中第一个被执行的<clinit>()方法的类肯定是java.lang.Object(因为Java中所有类的父类是Object类)
- 因为父类的<clinit>()方法先执行,所以也就意味着父类中定义的static语句块要优先于子类的变量赋值操作
- 如果一个类中没有静态变量或者是静态的语句块的时候,编译器可以不为这个类创建<clinit>()方法的
- 虚拟机会保证一个类的的<clinit>()方法在多线程环境中被正确的加锁和同步。多线程访问,一个访问,其他在访问的话会被阻塞。
使用
类实例化也初始化成功之后,这个类就是一个正常的类了。我们可以正常使用了。
卸载
当遇到以下几种情况的时候,类会被卸载
- 执行了System.exi()方法的时候
- 程序正常执行结束
- 程序在执行过程中遇到了异常或者是错误而异常终止
- 由于操作系统出现错误导致Java虚拟机进程终止
今天问题:
现在我们知道了一个Java类是怎么运行起来的了。那么请看下面代码,运行后输出的顺序是什么?
public class JvmDemo { public static void main(String[] args) { Son son = new Son(); FatherInterface fatherInterface = new SonInterFace(); fatherInterface.say("凯哥Java"); } } class Father{ static String st1 = "父类Father中的静态变量"; String str2 ="父类Father中的非静态变量"; static { System.out.println("当前执行了父类Father的静态代码块中的方法"); } { System.out.println("执行了父类Father类中的非静态代码块"); } public Father(){ System.out.println("执行了父类Father中的构造方法了"); } } class Son{ static String str1 = "子类Son中的静态变量"; String str2 = "子类Son中的非静态变量"; static{ System.out.println("执行了子类son中的静态代码块"); } { System.out.println("执行了子类Son中的非静态代码块"); } public Son(){ System.out.println("执行了子类son中的构造器方法"); } } interface FatherInterface{ static String str1 = "接口父类FatherInterface中的静态变量"; void say(String say); } class SonInterFace implements FatherInterface{ static String str1 = "子类SonInterFace中的静态变量"; String str2 = "子类SonInterFace中的非静态变量"; static{ System.out.println("执行了子类SonInterFace中的静态代码块"); } { System.out.println("执行了子类SonInterFace中的非静态代码块"); } public SonInterFace(){ System.out.println("执行了子类SonInterFace中的构造器方法"); } @Override public void say(String say) { System.out.println(FatherInterface.str1+"--say:"+say); } } |
运行后答案将在下一篇文章中揭晓。
下一篇预告:
因为这是第一篇,所以只是大致讲解了下一个类怎么加载过程。在下一篇文章中,咱们来讲解在加载阶段使用到类加载器、父类委派机制等、类在什么时候会被初始化等?。欢迎继续学习。