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中,然后在运行。输出结果。
一个java类的一生都会经历哪些步骤呢?
如下图:
编辑
在我们run的时候,AppTest.java类先经过编译后,编译成了AppTest.class文件。JVM把class文件加载到内存后需要经历:加载-验证-准备-解析-初始化-使用-卸载这七个阶段。
第一个问题:JVM在什么时候会加载一个类呢?起始也就是在什么时候会加载.class字节码文件到JVM的内存中去呢?上面我们写的,当我们run的时候,才执行的。所以答案就很明确了,就是在你代码中需要使用到这个类的时候,就去加载的。
具体每一步:
加载阶段是将class文件从磁盘或者jar等读到JVM内存中,并为其创建一个Class对象。任何一个类被使用时候系统都会为其创建一个Class对象的。
加载的同时将加载的这些数据转换成方法区中运行时数据(运行时候数据区:静态变量、静态代码块、常量池等),作为方法区数据的访问入口
这个很好理解的。我要想使用你,需要先得到你,是不是。结合上面我们自己写的AppTest类。在此阶段应该是:
编辑
扩展:
在类加载阶段JVM都做了什么?获取class文件方式都有哪些?
1.1:在类加载的时候JVM完成了以下:
(我们知道,在电脑的世界中,什么都是二进制形式存在的)
(这个话具体怎么理解,有哪位能留言教教凯哥)
(要想在内存中访问AppTest这个字节码类中的属性或者方法的时候,可以在内存中方法区找到对应的Class对象。这个Class就是入口)
关于方法区在后面文章中,凯哥会详细讲讲。
1.2:获取class文件的方式
在一个类运行生命周期内,类加载(加载获取类的二进制字节流)阶段,是可控性最强的阶段。因为在这个阶段,我们程序员可以使用系统提供的类加载去来加载完成,也可以使用自己自定义的类加载来完成.(类加载器在后面文章详细讲讲)
1.3:类加载的具体时机,在文章最后,凯哥会列出来。
将上一步加载到内存中的Class对象进行校验。确保加载的类的信息符合JVM的规范。确保没有安全方面的问题。
这个很好理解了,我要使用你,得到你好,我要检查你是不是符合标准的。如果不合法,就没法使用。
在此阶段如下图:
编辑
扩展:验证都验证哪些方面?
例如:是否已咖啡babe开头(0xCAFEBABE),主次版八号是否在当前JVM的处理范围内等等
比如你在JDK1.8下编译的class文件,放到JDK1.6版本的JVM中,有可能就运行不了的
例如:这个类如果有父类,是否实现了父类的抽象方法等.
例如:通过符号引用能找到对应点的类和方法。比如com.kaigejava.Person.getAge()
在比如:符号引用中类、属性、方法的访问性是否能被当前类访问等等。
准备阶段,就是给加载进来且验证通过的Class类分配空间的。这里是给类里面的变量(也就是static修饰的变量)分配空间的,同时给变量一个默认的初始值。
如下图:
编辑
在准备阶段时候static int m 被分配了4个字节的空间,且分配了默认初始值为0(注意默认初始值是0).
PS:int类型占用4个字节。int的默认值是0.如果是对象的话。默认为null
在此阶段AppTest.class如下图:
编辑
该阶段需要注意:
比如:final int x = 1;这个在此阶段就给赋值的就是1而不是0
解析是将常量池中的符号引用替换为直接引用(内存地址)的过程。
在此阶段AppTest类如下图:
编辑
扩展:
符号引用:
就是一组符号来描述目标的。可以是任何字面量。这个属于编译原理方面的东西。
比如:可以是一个类的完整类名字(com.kaigejava.Person)、字段的名称和描述符、方法的名称和描述等。
直接引用:
就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。比如指向方法区中某一个类的一个指针。
例如:在AppTest这个类中,有个static的静态变量p。这个静态变量p又是一个自定义的类型(com.kaigejava.Person),那么在经过解析阶段后,这个静态的p变量将是一个指针(比如0xddff1),这个指针指向该类在方法区的内存地址值。具体见凯哥后续文章,将会详细讲解。
编辑
到了此阶段(初始化阶段),JVM才开始真正的执行类中定义的Java代码。
当进行到初始化阶段的时候,就是执行类的构造器<clinit>()方法的过程。
类实例化也初始化成功之后,这个类就是一个正常的类了。我们可以正常使用了。
当遇到以下几种情况的时候,类会被卸载
现在我们知道了一个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中的构造器方法");}@Overridepublic void say(String say) {System.out.println(FatherInterface.str1+"--say:"+say);}} |
---|
编辑
编辑
运行后答案将在下一篇文章中揭晓。
下一篇预告:
因为这是第一篇,所以只是大致讲解了下一个类怎么加载过程。在下一篇文章中,咱们来讲解在加载阶段使用到类加载器、父类委派机制等、类在什么时候会被初始化等?。欢迎继续学习。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。