- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 回收
1.加载
JVM运行原理:
jvm什么时候会加载一个类,也就是说,啥时候会从“.class”字节码文件中加载这个类到JVM内存里来,答案就是代码中用到这个类的时候。如以下代码
public class ExamApp {
public static void main(String[] args){
Student student = new Student();
}
}
ExamApp作为启动类,在启动时就会被jvm加载到类加载器,同时,main()方法需要用到Student类去实例化一个对象,而Student类继承Person类,所以,Student.class和Person.class也会同时被加载到类加载器中。
2.验证
简单来说,这一步就是根据Java虚拟机规范,来校验你加载进来的“.class”文件中的内容,是否符合指定的规范。
3.准备
我们写的类,其实是有一些类变量(static 修饰),准备阶段就是给类分配一定的内存空间,然后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始值。比如下面的示例里,就会给 age
这个类变量分配内容空间,给一个“0”这个初始值,headPicUrl
给一个null初始值。
public class Person {
public static String headPicUrl = ConfigUtil.get("head.pic.url");
public static int age;
private String name;
}
4.解析
解析实际上是把符号引用替换为直接引用的过程,其实这个部分的内容很复杂,设计JVM底层,不作过深解读。
5.初始化–核心阶段
- 在类的准备阶段,JVM会为类分配内存空间,给类的变量赋初始值,那么,类的初始化阶段主要是做什么呢?
public class Person {
public static String headPicUrl = ConfigUtil.get("head.pic.url");
public static int age;
private String name;
}
headPicUrl
这个变量,我们是打算通过 ConfigUtil.get(“head.pic.url”) 获取一个值并且赋值给它的。但是在准备阶段会执行这段代码逻辑吗?答案是否定的,准备阶段只是开辟一块内存空间并且赋予初始值。执行类的初始化代码是在初始化阶段,也就是说这一阶段,会从配置文件里面读取一个值,并且赋给headPicUrl。
- 另外,static静态代码块也会在这个阶段来执行,如以下代码:类初始化的时候,调用
loadStudent()
方法从磁盘中加载数据副本,并且放在静态变量studentMap
中:
public class Teacher extends Person{
public static Map<Integer,Student> studentMap;
static {
loadStudent();
}
public static void loadStudent(){
studentMap = new HashMap<Integer, Student>();
}
}
- 什么时候会初始化一个类?
- 实例化类的对象的时候,如
new Student()
的时候,就会触发类的加载到初始化的全过程,把这个
类准备好,然后再实例化一个对象出来; - 包含main()方法的主类,如
ExamApp
这个类,必须是立马初始化的; - 如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类。如上面的
Person
这个类。
- 实例化类的对象的时候,如
- 类的执行顺序
概念 | 格式 | 执行时机 | 作用 |
---|---|---|---|
静态代码块 | static{ System.out.println(“静态代码块”); } | 静态代码块在类被加载的时候就运行了,而且只运行一次,并且优先于各种代码块以及构造函数。如果一个类中有多个静态代码块,会按照书写顺序依次执行。 | 需要在项目启动的时候就执行 |
构造代码块 | { System.out.println(“构造代码块”); } | 构造代码块在创建对象时被调用,每次创建对象都会调用一次,但是优先于构造函数执行。注意:构造代码块依托于构造函数,也就是说,如果你不实例化对象,构造代码块是不会执行的。 | 对对象进行初始化,并且只要创建一个对象,构造代码块都会执行一次 |
构造函数 | public Student(){} | 通过new运算符在创建对象时才会自动调用,默认先调用父类的无参构造函数 | 对对象进行初始化 |
普通代码块 | public void sayHello(){ { System.out.println(“普通代码块”); } } | 普通代码块是在方法体中定义的。且普通代码块的执行顺序和书写顺序一致。 |
Persion类:
public class Person {
/**
* 静态变量
*/
public static int age;
/**
* 构造代码块
*/
{
System.out.println("Person no static code block :" + age);
}
/**
* 静态代码块
*/
static{
System.out.println("Person static's count:" + age);
}
/**
* 有参构造函数
* @param a
*/
Person(int a){
System.out.println("Person init one parameter");
}
/**
* 无参构造函数
*/
Person(){
System.out.println("Person init");
}
}
Student类:
public class Student extends Person{
{
System.out.println("Student no static code block :" + age);
}
static {
System.out.println("Student static 1");
}
public Student() {
System.out.println("Student init:" + age);
}
static {
System.out.println("Student static 2");
}
public static void main(String[] args){
System.out.println("开始执行main方法==");
Student student = new Student();
System.out.println("结束执行main方法==");
}
}
打印输出结果:
Person static's count:0
Student static 1
Student static 2
开始执行main方法==
Person no static code block :0
Person init
Student no static code block :0
Student init:0
结束执行main方法==
6. 使用
经过上面几步之后,类就可以通过实例化被使用了,虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,然后检查这个符号引用代表的类是否已被加载,连接(不要求类初始化)。如果已经加载,连接,不管类是否初始化,则进行实例化。如果没有则先执行类加载的三个自然过程(包括类初始化),等三阶段的最后阶段类初始化执行完之后再进行类实例化。
7. 回收
垃圾回收后续章节会详细讲解,这里不做深入描述。