特此说明:本系列所有JVM都是针对Hotspot展开学习,JDK1.8
根据冯.诺依曼定义的计算机模型,任何程序都要加载到内存中才能与CPU进行交流。编写好的.java文件是源代码,机器不能直接指向,需要将其编译成字节码或是机器码文件。
字节码
字节码必须通过类加载过程到JVM环境后才可以执行,执行有三种模式
- 解释执行
代码执行一行动态的翻译和执行,并不是执行前一次性完成翻译。所以执行速度慢,效率低,因为解释器所以跨平台性好
- JIT编译执行
一次性编译。将java字节码动态编译成机器码,直接发送给处理器执行。执行速度快,效率高,跨平台性差。
- JIT编译与解释器混合执行(主流JVM默认模式)
解释器在启动时先解释执行,省去编译时间,随着时间推进,JVM统计热点代码,然后基于JIT动态编译,将热点代码转换成机器码。即时编译流程如下图
类的加载过程
1.装载
通过一个类的全限定名获取对应的二进制字节流,既然需要去寻找,那也就需要一个“寻找器”,在java中称之为类加载器,并且将寻找加载这个动作是放在jvm外部去实现的,以便应用程序可以获得所需要的类。
- 此阶段读取类文件产生的二进制流
- 将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构(由于此时只是加载类,并未实例化,因此只能将静态数据放进内存)
- 在Java堆中生成一个代表这个类的java.lang.Class对象
装载阶段完成之后,运行时数据区的方法区和堆就有了数据。
注:Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区。方法区是一个抽象定义,可以想象成接口;元空间或永久代是具体的实现技术
2.链接
该阶段包括三个步骤。
2.1 验证是更详细的校验,比如语法是否正确,类型是否正确等;
2.2 准备阶段是为静态变量分配内存,设定默认值;
2.3 解析是把jvm常量池中的符号引用转变成直接引用,也就是直接指向目标的指针,相对偏移量。有了直接引用,内存中也就存在引用的目标。
3.初始化
执行类构造器<clinit>方法的过程。
在Java中对类变量进行初始值设定有两种方式:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
4.使用
那初始化过程是什么时候执行的呢?
主动引用
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如 Class.forName(“com.carl.Test”) )
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类( JvmCaseApplication ),直接使用 java.exe
- 运行某个主类
被动引用
public class Demo {
public static void main(String[] args) {
// String a = Son.staticMsg ; //只会引起父类的初始化
// String b = Parent.finalMsg ; //不会引起类的初始化
Son[] arry = new Son[1]; //不会引起类初始化
}
}
class Parent {
static {
System.out.println("parent init static method");
}
public static String staticMsg = "static msg" ;
public static final String finalMsg = "final msg" ;
public String nomalMsg = "normal msg" ;
}
class Son extends Parent{
static {
System.out.println("son stataic method...");
}
}
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 定义类数组,不会引起类的初始化。
- 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化
的),常量值存储在JVM内存中的常量区中,在类不加载时即可访问。
5.卸载
在类使用完之后,如果满足下面的情况,类就会被卸载:
- 加载该类的ClassLoader已经被回收。
- 堆中不存在任何实例,都已经被垃圾回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class
对象,因此这些Class对象始终是可触及的。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
类的加载三种方式
- 全盘负责
当类加载器去加载A类,A类又在同时依赖或者引起其他类,都应该一同被加载
- 双亲委派(父类委托)
由更高级别的类加载器去加载
- 缓存机制
加载过类都存放在内存中,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用,这也就是我们的类变量只会被初始化一次的由来。