Java运行程序又被称为WORA(Write Once Run Anywhere,在任何地方运行只需写入一次),意味着我们程序员小哥哥可以在任何一个系统上开发Java程序,但是却可以在所有系统上畅通运行,无需任何调整,大家都知道这是JVM的功劳,但具体是JVM的哪个模块或者什么机制实现这一功能呢?
JVM(Java Virtual Machine, Java虚拟机)作为运行java程序的运行时引擎,也是JRE(Java Runtime Environment, Java运行时环境)的一部分。
说起它想必不少小伙伴任处于似懂非懂的状态吧,说实话,着实是块难啃的骨头。但古语有云:千里之行,始于足下。我们今天主要谈谈,为什么JVM无需了解底层文件或者文件系统即可运行Java程序?
--这主要是类加载机制在运行时将Java类动态加载到JVM的缘故。

当我们编译.java文件时,Java编译器会生成与.java文件同名的.class文件(包含字节码)。当我们运行时,.class文件会进入到各个步骤,这些步骤共同描绘了整个JVM,上图便是一张精简的JVM架构图。
今天,我们的主角就是类加载机制 - 说白了,就是将.class文件加载到JVM内存中,并将其转化为java.lang.Class对象的过程。这对这个过程,我们可以细分为如下几个阶段:
  • 加载
  • 连接(验证,准备,解析)
  • 初始化

注意: 正常场景下,加载的流程如上。但是Java语言本身支持运行时绑定,所以解析阶段是用可能放在初始化之后进行的,称为动态绑定或者晚期绑定。
 

I.类加载流程

1. 加载

加载:通过类的全局限定名找到.class文件,并利用.class文件创建一个java.lang.Class对象。
  • 根据类的全局限定名找到.class文件,生成对应的二进制字节流。
  • 将静态存储结构转换为运行时数据结构,保存运行时数据结构到JVM内存方法区中。
  • JVM创建java.lang.Class类型的对象,保存于堆(Heap)中。利用该对象,可以获取保存于方法区中的类信息,例如:类名称,父类名称,方法和变量等信息。
For Example:

package com.demo;

import java.lang.reflect.Field;
import java.lang.reflect.Method; public class ClassLoaderExample {
public static void main(String[] args) {
StringOp stringOp = new StringOp(); System.out.println("Class Name: " + stringOp.getClass().getName());
for(Method method: stringOp.getClass().getMethods()) {
System.out.println("Method Name: " + method.getName());
}
for (Field field: stringOp.getClass().getDeclaredFields()) {
System.out.println("Field Name: " + field.getName());
}
}
}
StringOp.class
package com.demo;

public class StringOp {
private String displayName;
private String address; public String getDisplayName() {
return displayName;
} public String getAddress() {
return address;
}
}

output:

Class Name: com.demo.StringOp
Method Name: getAddress
Method Name: getDisplayName
Field Name: displayName
Field Name: address
注意:对于每个加载的.class文件,仅会创建一个java.lang.Class对象.
StringOp stringOp1 = new StringOp();
StringOp stringOp2 = new StringOp();
System.out.println(stringOp1.getClass() == stringOp2.getClass());
//output: true

2. 连接

2.1 验证

验证:主要是确保.class文件的正确性,由有效的编译器生成,不会对影响JVM的正常运行。通常包含如下四种验证:
  • 文件格式:验证文件的格式是否符合规范,如果符合规范,则将对应的二进制字节流存储到JVM内存的方法区中;否则抛出java.lang.VerifyError异常。
  • 元数据:对字节码的描述信息进行语义分析,确保符合Java语言规范。例如:是否有父类;是否继承了不允许继承的类(final修饰的类);如果是实体类实现接口,是否实现了所有的方法;等。。
  • 字节码:验证程序语义是否合法,确保目标类的方法在被调用时不会影响JVM的正常运行。例如int类型的变量是否被当成String类型的变量等。
  • 符号引用:目标类涉及到其他类的的引用时,根据引用类的全局限定名(例如:import com.demo.StringOp)能否找到对应的类;被引用类的字段和方法是否可被目标类访问(public, protected, package-private, private)。这里主要是确保后续目标类的解析步骤可以顺利完成。

2.2 准备

准备:为目标类的静态字段分配内存设置默认初始值(当字段被final修饰时,会直接赋值而不是默认值)。需要注意的是,非静态变量只有在实例化对象时才会进行字段的内存分配以及初始化。
public class CustomClassLoader {
//加载CustomClassLoader类时,便会为var1变量分配内存
//准备阶段,var1赋值256
public static final int var1 = 256;
//加载CustomClassLoader类时,便会为var2变量分配内存
//准备阶段,var2赋值0, 初始化阶段赋值128
public static int var2 = 128;
//实例化一个CustomClassLoader对象时,便会为var1变量分配内存和赋值
public int var3 = 64;
}
注意:静态变量存在方法区内存中,实例变量存在堆内存中。
这里简单贴一下Java不同变量的默认值:

数据类型
默认值
int
0
float
0.0f
long
0L
double
0.0d
short
(short)0
char
'\u0000'
byte
(byte)0
String
null
boolean
false
ArrayList
null
HashMap
null

2.3 解析

解析:将符号引用转化为直接引用的过程。
  • 符号引用(Symbolic Reference):描述所引用目标的一组符号,使用该符号可以唯一标识到目标即可。比如引用一个类:com.demo.CustomClassLoader,这段字符串就是一个符号引用,并且引用的对象不一定事先加载到内存中。
  • 直接引用(Direct Reference):直接指向目标的指针,相对偏移量或者一个能间接定位到目标的句柄。根据直接引用的定义,被引用的目标一定事先加载到了内存中。

3. 初始化

前面的准备阶段时,JVM为目标类的静态变量分配内存并设置默认初始值(final修饰的静态变量除外),但到了初始化阶段会根据用户编写的代码重新赋值。换句话说:初始化阶段就是JVM执行类构造器方法<clinit>()的过程。
 
<init>()<clinit>()从名字上来看,非常的类似,或许某些童鞋会给双方画上等号。然则,对于JVM来说,虽然两者皆被称为构造器方法,但此构造器非彼构造器。
  • <init>():对象构造器方法,用于初始化实例对象
    • 实例对象的constructor(s)方法,和非静态变量的初始化;
    • 执行new创建实例对象时使用。
  • <clinit>():类构造器方法,用于初始化类
    • 类的静态语句块和静态变量的初始化;
    • 类加载的初始化阶段执行。
For Example:

public class ClassLoaderExample {
private static final Logger logger = LoggerFactory.getLogger(ClassLoaderExample.class);//<clinit>
private String property = "custom"; //<init> //<clinit>
static {
System.out.println("Static Initializing...");
} //<init>
ClassLoaderExample() {
System.out.println("Instance Initializing...");
} //<init>
ClassLoaderExample(String property) {
this.property = property;
System.out.println("Instance Initializing...");
}
}
查看对应的字节码:
public ClassLoaderExample();  <init>
Code:
0 aload_0 //将局部变量表中第一个引用加载到操作树栈
1 invokespecial #1 <java/lang/Object.<init>> //调用java.lang.Object的实例初始化方法
4 aload_0 //将局部变量表中第一个引用加载到操作树栈
5 ldc #2 <custom> //将常量custom从常量池推送至栈顶
7 putfield #3 <com/kaiwu/ClassLoaderExample.property> //设置com.kaiwu.ClassLoaderExample实例对象的property字段值为custom
10 getstatic #4 <java/lang/System.out> //从java.lang.System类中获取静态字段out
13 ldc #5 <Instance Initializing...> //将常量Instance Initializing从常量池第5个位置推送至栈顶
15 invokevirtual #6 <java/io/PrintStream.println> //调用java.io.PrintStream对象的println实例方法
18 return //返回
public ClassLoaderExample(String property);  <init>
Code:
0 aload_0 //将局部变量表中第一个引用加载到操作树栈
1 invokespecial #1 <java/lang/Object.<init>> //调用java.lang.Object的实例初始化方法
4 aload_0 //将局部变量表中第一个引用加载到操作树栈
5 ldc #2 <custom> //将常量custom从常量池第二个位置推送至栈顶
7 putfield #3 <com/kaiwu/ClassLoaderExample.property> //将常量custom赋值给com.kaiwu.ClassLoaderExample实例对象的property字段
10 aload_0 //将局部变量表中第一个引用加载到操作树栈
11 aload_1 //将局部变量表中第二个引用加载到操作树栈
12 putfield #3 <com/kaiwu/ClassLoaderExample.property> //将入参property赋值给com.kaiwu.ClassLoaderExample实例对象的property字段
15 getstatic #4 <java/lang/System.out> //从java.lang.System类中获取静态字段out
18 ldc #5 <Instance Initializing...> //将常量Instance Initializing从常量池第5个位置推送至栈顶
20 invokevirtual #6 <java/io/PrintStream.println> //调用java.io.PrintStream对象的println实例方法
23 return //返回
<clinit>():

Code:
0 ldc #7 <com/kaiwu/ClassLoaderExample> //将com.kaiwu.ClassLoaderEexample的class_info常量从常量池第七个位置推送至栈顶
2 invokestatic #8 <org/slf4j/LoggerFactory.getLogger> //从org.slf4j.LoggerFactory类中获取静态字段getLogger
5 putstatic #9 <com/kaiwu/ClassLoaderExample.logger> //设置com.kaiwu.ClassLoaderExample类的静态字段logger
8 getstatic #4 <java/lang/System.out> //从java.lang.System类中获取静态字段out
11 ldc #10 <Static Initializing...> //将常量Static Initializing从常量池第10个位置推送至栈顶
13 invokevirtual #6 <java/io/PrintStream.println> //调用java.io.PrintStream对象的println实例方法
16 return //返回

II. 类加载器

1. 类加载器ClassLoader

java.lang.ClassLoader本身是一个抽象类,它的实例用来加载Java类。这里如果细心的小伙伴就会发现,java.lang.ClassLoader的实例用来加载Java类,但是它本身也是一个Java类,谁来加载它?先有鸡,还是先有蛋??
不急,待我们细细说来!!
首先,我们看一个简单的示例,看看都有哪些不同的类加载器:
public static void printClassLoader() {
// StringOP:自定义类
System.out.println("ClassLoader of StringOp: " + StringOp.class.getClassLoader());
// com.sun.javafx.binding.Logging:Java核心类扩展的类
System.out.println("ClassLoader of Logging: " + Logging.class.getClassLoader());
// java.lang.String: Java核心类
System.out.println("ClassLoader of String: " + String.class.getClassLoader());
}

output:

ClassLoader of StringOp: sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader of Logging: sun.misc.Launcher$ExtClassLoader@7c3df479
ClassLoader of String: null
从输出可以看出,这里有三种不同的类加载器:应用类加载器(Application/System class loader), 扩展类加载器(Extension class loader)以及启动类加载器(Bootstrap class loader)。
  • 启动类加载器:本地代码(C++语言)实现的类加载器,负责加载JDK内部类(通常是$JAVA_HOME/jre/lib/rt.jar$JAVA_HOME/jre/lib目录中的其他核心类库)或者-Xbootclasspath选项指定的jar包到内存中。该加载器是JVM核心的一部分,以本机代码编写,开发者无法获得启动类加载器的引用,所以上述java.lang.String类的加载为null。此外,该类充当所有其他java.lang.Class Loader实例共同的父级(区别为是否为直接父级),它加载所有直接子级的java.lang.ClassLoader类(其他子类逐层由直接父级类加载器加载)。
  • 扩展类加载器:启动类加载器的子级,由Java语言实现的,用来加载JDK扩展目录下核心类的扩展类(通常是$JAVA_HOME/lib/ext/*.jar)或者-Djava.ext.dir系统属性中指定的任何其他目录中存在的类到内存中。由sun.misc.Launcher$ExtClassLoader类实现,开发者可以直接使用扩展类加载器。
  • 应用/系统类加载器:扩展类加载器的子级,负责将java -classpath/-cp($CLASSPATH)或者-Djava.class.path变量指定目录下类库加载到JVM内存中。由sun.misc.Launcher$AppClassLoader类实现,开发者可以直接使用系统类加载器。

2. 类加载器的类图关系

 
通过上文的分析,目前常用的三种类加载器分别为:启动类加载器,扩展类加载器以及应用/系统加载器。但是查看源码的类图关系,可以发现AppClassLoder和ExtClassLoader都是sun.misc.Laucher(主要被系统用于启动主应用程序)这个类的静态内部类,并且两个类之间也不存在继承关系,那为何说应用/系统类加载器是扩展类加载器的子级呢?
源码分析(JDK1.8): sun.misc.Laucher
Launcher.ExtClassLoader.getExtClassLoader():获取ExtClassLoader实例对象。
Launcher.AppClassLoader.getAppClassLoader(final ClassLoader var0): 根据ExtClassLoader实例对象获取AppClassLoader实例对象。
Launcher.AppClassLoader(URL[] var1, ClassLoader var2): 根据$CLASSPATH和ExtClassLoader实例对象创建AppClassLoader实例对象。
 
层层剖析,可见虽然AppClassLoader类和ExtClassLoader类虽然并无继承(父子)关系,但是AppClassLoader类实例化出来的对象却是ExtClassLoader实例对象的子级。
 
一般而言,在Java的日常开发中,通常是由上述三种类加载器相互配合完成的,当然,也可以使用自定义类加载器。需要注意的是,这里的JVM对.class文件是按需加载的或者说是Lazy模式,当需要使用某个类时才会将该.class加载到内存中生成java.lang.Class对象,并且每个.class文件只会生成一个java.lang.Class对象
至于几种加载器时如何配合的,这里就不得不提JVM采用的双亲委派机制了。
 

3. 双亲委派机制

核心思想:自底向上检查类是否已加载,自顶向下尝试加载类。

 

使用双亲委派模式的优势
  • 使用双亲委派模式可以避免类的重复加载:当父级加载器已经加载了目标类,则子加载器没有必要再加载一次。
  • 避免潜在的安全风险:启动类加载器是所有其他加载器的共同父级,所以java的核心类库不会被重复加载,意味着核心类库不会被随意篡改。例如我们自定义名为java.lang.String的类,通过双亲委派模式进行加载类,通过上述流程图,启动类加载器会发现目标类已经加载,直接返回核心类java.lang.String,而不会通过应用/系统类加载器加载自定义类java.lang.String。当然,一般而言我们是不可以加载全局限定名与核心类同名的自定义类,否则会抛出异常:java.lang.SecurityException: Prohibited package name: java.lang
源码分析(JDK1.8):java.lang.ClassLoader.class
loadClass(String name): 根据类的全局限定名称,由类加载器检索,加载,并返回java.lang.Class对象。

 
根据源码,我们发现流程如下:
  • 当加载器收到加载类的请求时,首先会根据该类的全局限定名查目标类是否已经被加载,如果加载则万事大吉;
  • 如果没有加载,查看是否有父级加载器,如果有则将加载类的请求委托给父级加载器;
  • 依次递归;
  • 直到启动类加载器,如果在已加载的类中依旧找不到该类,则由启动类加载器开始尝试从所负责的目录下寻找目标类,如果找到则加载到JVM内存中;
  • 如果找不到,则传输到子级加载器,从负责的目录下寻找并加载目标类;
  • 依次递归;
  • 直到请求的类加载器依旧找不到,则抛出java.lang.ClassNotFoundException异常。
如果看文字略感不清晰的话,请对照源码上面的流程图结合来看。
findLoadedClass(String name): 从当前的类加载器的缓存中检索是否已经加载目标类。findLoadedClass0(name)其实是底层的native方法(C编写)。

 

findBootstrapClassOrNull(String name): 从启动类加载器缓存中检索目标类是否已加载;如果没有加载,则在负责的目录下($JAVA_HOME/jre/lib/rt.jar)所寻该类文件(.class)并尝试加载到内存中,并返回java.lang.Class对象,如果没有找到则返回null。findBootstrapClass(String name)其实是底层的natvie方法。

findClass(String name): 从加载器负责的目录下,根据类的全局限定名查找类文件(.class),并返回一个java.lang.Class对象。根据源码我们可以发现在ClassLoader这个类中,findClass没有任何的逻辑,直接抛出java.lang.ClassNotFoundException异常,所以,我们使用的类加载器都需要重写该方法。

defineClass(String name, byte[] b, int off, int len): 当找到.class文件后获取到对应的二进制字节流(byte[]),defineClass函数将字节流转换为JVM可以理解的java.lang.Class对象。需要注意的是,该方法的入参是二进制的字节流,这不一定是.class文件形成的,也可能是通过网络等传输过来的。

resolveClass(Class<?> c): 该方法可以使加载完类时,同时完成链接中的解析步骤,使用的是native方法。如果这里不解析,则在初始化之后再解析,称为晚期绑定。

上述的源码让我们可以很清晰的理解双亲委派的具体流程。
但是在ClassLoader.class中并没有findClass(String name)方法的具体实现,仅仅是抛出java.lang.ClassNotFoundException异常,需要实体类进行重写,这里以jave.netURLClassLoader.class实体类为例,分析源码是如何实现类的搜寻与加载。
源码分析(JDK1.8): java.net.URLClassLoader.class

流程分析:根据类的全局限定名(例如:com.kaiwu.CustomClassLoader),转换为对应的相对存储路径(com/kaiwu/CustomClassLoader.class),相应的加载器在对应的目录下寻找目标.class文件(这里是应用/系统加载器,所以该文件的具体路径为$CLASSPATH/com/kaiwu/CustomClassLoader.class),利用ucp(sum.misc.URLClassPath)对象获取该文件的资源,并将目标资源转换为系统可读的二进制字节流(byte[]),通过defineClass()函数将字节流转换为JVM可读的java.lang.Class对象,并返回。
 
案例分析:
请求加载自定义类com.kaiwu3.CustomClassLoader
请求加载扩展类com.sum.javafx.binding.Logging

 

调试分析:
根据类的全局限定名(例如:com.kaiwu3.CustomClassLoader)转化为存储目录(com/kaiwu/CustomClassLoade.class),在应用/系统类加载器负责的目录下($CLASSPATH)找到目标.class文件。

 

将目标文件转化为java.lang.Class对象(Class@800),并利用应用/系统类加载器(Laucher$AppClassLoader@512)加载目标对象到内存中,父级加载器为扩展类加载器(Laucher$ExtClassLoader@346)。

 

根据类的全局限定名(例如:com.sum.javafx.binding.Logging)转化为存储目录(com/sum/javafx/binding/Logging.class),在扩展类类加载器负责的目录下($JAVA_HOME/jre/lib/ext/jfxrt.jar/)找到目标.class文件。

 

将目标文件转化为java.lang.Class对象(Class@793),并利用扩展类加载器(Launcher$ExtClassLoader@346)加载目标对象到内存中,父级类加载器为启动加载器(null)。

 

 
总体而言,JVM的类加载机制并非想象中那么复杂,若静下心来,仔细琢磨一二,亦感其中妙趣。
以上为个人解读与理解,如有不明之处,望各位大佬不吝赐教。

作者:吴家二少
本文欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接 

一文教你读懂JVM类加载机制的更多相关文章

  1. 一文读懂Java类加载机制

    Java 类加载机制 Java 类加载机制详解. @pdai Java 类加载机制 类的生命周期 类的加载:查找并加载类的二进制数据 连接 验证:确保被加载的类的正确性 准备:为类的静态变量分配内存, ...

  2. 一夜搞懂 | JVM 类加载机制

    前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习导图 一.为什么要学习类加载机制? 今天想跟大家唠嗑唠嗑Java的类加载机制,这是Java的一个很重要的创 ...

  3. 一文教你读懂JVM的类加载机制

    Java运行程序又被称为WORA(Write Once Run Anywhere,在任何地方运行只需写入一次),意味着我们程序员小哥哥可以在任何一个系统上开发Java程序,但是却可以在所有系统上畅通运 ...

  4. 搞懂JVM类加载机制

    有这样一道面试题: class Singleton{ private static Singleton singleton = new Singleton(); public static int v ...

  5. 深入理解JVM虚拟机6:深入理解JVM类加载机制

    深入理解JVM类加载机制 简述:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 下面我们具体 ...

  6. JVM基础系列第7讲:JVM 类加载机制

    当 Java 虚拟机将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析.运行等整个过程,这个过程我们叫:Java 虚拟机的类加载机制.JVM 虚拟机执行 class 字节 ...

  7. JVM总结(四):JVM类加载机制

    这一节我们来总结一下JVM类加载机制.具体目录如下: 类加载的过程 类加载过程概括 说说引用 详解类加载全过程: 加载 验证 准备 解析 初始化 虚拟机把描述类的数据从Class文件加载到内存,并对数 ...

  8. JVM 类加载机制详解

    如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lan ...

  9. Java虚拟机(四):JVM类加载机制

    1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...

随机推荐

  1. [转] Java Agent使用详解

    以下文章来源于古时的风筝 ,作者古时的风筝 我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着.但其实我们一直在用它,而且接触的机会非常多.下面这些技术都使用了 Java Agent ...

  2. Proteus 8使用 1新建一个Proteus工程

    新建一个Proteus工程 下一步 创建部分结束,可以看到两部分-->原理图与源代码. 首先按下F7或从“构建”菜单中选择“构建工程” 之后切换到原理图窗口 按下F12或点击窗口最左下角的“运行 ...

  3. nginx多个server的配置,同一端口

    nginx多个server的配置,同一端口 #user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/e ...

  4. 验证Kubernetes YAML的最佳实践和策略

    本文来自Rancher Labs Kubernetes工作负载最常见的定义是YAML格式的文件.使用YAML所面临的挑战之一是,它相当难以表达manifest文件之间的约束或关系. 如果你想检查所有部 ...

  5. 8 Java 条件逻辑语句

    生活中,我们经常需要先做判断,然后才决定是否要做某件事情.例如,在上学的时候,如果期末考试成绩在全校能拿到前100名,则奖励一个 iPhone 11 .对于这种“需要先判断条件,条件满足后才执行的情况 ...

  6. Linux安装python 3

    方法: 1.官网下载地址:http://www.python.org/download/ 2.rz -y 上传到Linux  解压tar -xvzf Python-3.5.1.tgz  进入目录   ...

  7. C#LeetCode刷题之#263-丑数(Ugly Number)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3862 访问. 编写一个程序判断给定的数是否为丑数.丑数就是只包含 ...

  8. JavaScript - async/await 基础示例

    一个函数如果被 async 修饰,无论内部是否有 await的异步操作,都会返回一个 Promise 对象 demo 1 async function basicAsync() { let resul ...

  9. LeetCode 763划分字母区间 详解

    题目详情 字符串 S 由小写字母组成.我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段.返回一个表示每个字符串片段的长度的列表. 示例 1: 输入: S = "ab ...

  10. 设计模式详解及Python实现

    设计模式及Python实现 设计模式是什么? Christopher Alexander:"每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心*.这样你就能一次又一 ...