类加载过程包含:加载、验证、准备、解析和初始化 ,一共包括5
个阶段。
简单来说就是将java类的字节码文件加载到机器内存中。在加载类时,Java虚拟机必须完成以下3件事情:
Metaspace
元空间区的运行时存储结构。Class
对象,作为元空间区中该类各种数据的访问入口。确保 Class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
为类的静态分配内存,并将其初始化为默认值当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。Java虚拟机为各类型变量默认的初始值如表所示。
类型 |
默认初始值 |
byte |
0 |
short |
0 |
int |
0 |
long |
0L |
float |
0.0f |
double |
0.0 |
char |
\u0000 |
boolean |
false |
reference |
null |
没有对static fianl修饰的变量进行赋值,因为final修饰的变量已经在编译阶段就进行了赋默认值,而在准备阶段进行的是显式赋值
将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java
的动态绑定。
符号引用就是一些自变量的引用,和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在Class类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下println()方法被调用时,系统需要明确知道该方法的位置。
初始化阶段才真正开始执行类中定义的 Java
程序代码。初始化阶段是虚拟机执行类构造器
方法的过程。
对于
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的
正是因为函数
如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行
main()
方法的类),虚拟机会先加载这个类。JDK8
新加入的默认方法(被 default
关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。类加载器是Java虚拟机中的一个子系统,用于将类的字节码文件加载到内存中,并转换为Java的类对象。Java虚拟机需要根据类的全名来定位并加载类的字节码文件,而类加载器负责从文件系统、网络等位置查找类文件,并加载到Java虚拟机中。
Java虚拟机中有三种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器。
它负责加载Java的核心类(如String、System等)。它比较特殊,因为它是由原生C++代码实现的,并不是java.lang.ClassLoader的子类。启动类加载器无法被 Java
程序直接引用,所以下面的运行结果为null:
它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext)中JAR包的类,我们可以通过把自己开发的类打包成JAR文件放入扩展目录来为Java扩展核心类以外的新功能。
它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader来获取系统类加载器:
虚拟机遇到一条 new
指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java
堆中划分出来。内存分配的查找方式有 “指针碰撞” 和 “空闲列表” 两种。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java
代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java
程序的视角来看,对象创建才刚开始,
构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new
指令之后会接着执行
构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。