前言
从本篇文章开始我们来探讨JVM相关的知识,内容附带JVM的启动,JVM内存模型,JVM垃圾回收机制,JVM参数调优等,跟着文章一步一步走相信你对JVM会有一个不一样的认识,如果觉得文章对你有所帮助请给个好评吧。
JVM类加载系统
1.JVM的启动流程
当我们编写好Java源代码之后如hello.java ,通过javac命令把hello.java文件编译为hello.class文件,然后通过java.exe去执行hello.class 字节码文件,这个时候Java会启动JVM虚拟机,虚拟机是通过jvm.ddl文件创建的,底层是由C++实现的,具体的启动流程如下
对上图做一个步骤解释:
- 编译源代码:使用Java编译器(javac)将Hello.java编译成字节码文件(Hello.class)。这一步将源代码转换成JVM能够理解的指令集。
- 启动JVM:通过命令行界面调用java.exe来启动Java虚拟机(底层是由C++来实现的),java.exe是JVM的入口点,负责加载和运行Java应用程序。
- 创建启动类加载器:JVM在启动时,会首先加载BootstrapClassLoader启动类加载器(这个类加载器也是c++实现的),它是Java类加载器体系的最顶层加载器,负责加载核心类库。
- 创建启动器:启动类加载器会加载Launcher , C++会调用Java代码创建JVM启动器:sun.misc.Launcher ,该启动器的作用是用来加载器其他的类加载器,比如:AppClassLoader应用类加载器
- 加载应用类:Launcher 会根据当前类Hello.class找到其ClassLoader并加装它,也就是AppClassLoader应用类加载器,它负责加载用户自定义的Java类(如Hello类)
- 加载Class : 通过AppClassLoader 加载Hello.class字节码文件
- 执行Main方法:找到class类中的main方法,JVM通过调用类的main方法作为程序的入口点来执行Java程序(这一步也是c++调用的)
- 运行Java程序:Java程序开始执行,直到遇到main方法结束或者发生异常而终止。
2.类加载器
当我们在Java中编写代码并引用某个类时,这个类是如何被加载到JVM中的呢?这涉及到JVM的类加载器(ClassLoader)以及类加载的流程和双亲委派机制。下面我将详细解释这些概念。
JVM的类加载器是负责将类的字节码文件(通常是.class文件)加载到JVM中,并为其生成对应的Class对象的过程。类加载器是Java运行时环境的一部分,是Java程序获取字节码文件的重要途径。
JVM提供了三种主要的类加载器:
- 引导类加载器(Bootstrap ClassLoader):这是JVM的内置类加载器,主要负责加载Java的核心类库,一般对应JAVA_HOME/lib 目录中的JAR包,如java.lang.*、java.util.*等。由于它并不是Java类库的一部分,而是JVM自身的实现,所以它并不继承自java.lang.ClassLoader。
- 扩展类加载器(Extension ClassLoader):这是Java的标准扩展类加载器,负责加载Java的扩展类库,一般对应JAVA_HOME/lib/ext目录中的JAR包。它是java.lang.ClassLoader的子类,由sun.misc.Launcher$ExtClassLoader实现。
- 应用类加载器:Application ClassLoader:也称为系统类加载器(ApplicationClassLoader)或默认类加载器(System ClassLoader),负责加载应用程序的类路径(classpath)下的所有类包括pom.xml导入的jar。它是java.lang.ClassLoader的子类,由sun.misc.Launcher$AppClassLoader实现。在Java应用程序中,我们通常使用的就是这个类加载器。
除了以上三种主要的类加载器,我们还可以自定义类加载器,通过继承java.lang.ClassLoader类并重写其相关方法来实现。
注意:这些类加载器并没有实际上的继承关系
3.双亲委派机制
JVM的双亲委派机制是Java语言服务器级别的安全策略,主要思想是在类加载过程中,子类委托给父类加载器优先加载
,即:当一个类需要被加载时,它首先会委托给其父类加载器进行加载。如果父类加载器无法加载该类,那么子类加载器才会尝试自己加载,如果父类加载过了子类就不会再加载了
。这种层层委派的方式确保了类加载的有序性和唯一性
。
双亲委派的好处在于:
- 类加载安全策略:例如我们自己编写一个 java.lang.String 是否会覆盖java自带的String类呢?答案是无法覆盖,因为BootStrapClassLoader优先加载了java.lang.String 后,AppClassLoader在加载我们自己的java.lang.String的时候会检查重复加载,也就不会再加载了。保证了类的唯一性
- 保证有序性 : JVM启动必须要加载一些基础的类,比如:Object.clas 这些基础的类会通过BootstrapClassLoader 和 ExtClassLoader 优先加载后,再加载我们自己的类,否则JVM无法启动,启动也会报错。
下面是AppClassLoader的类加载源码,可以看得出来JVM在加载类之前会查找类是否已经被加载,如果没加载就会调用 parent.loadClass
让父类优先加载,如果加载失败再调用findClass
自己加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//1.首先,检查类是否已加载
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果父类加载器不为空,则优先委派父类进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类加载器为空,则查找 Bootstrap 类加载器,如果找不到则返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//如果c == null 说明父类加载失败,则自己加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
4.打破双亲委派
在JVM中,类加载器的双亲委派机制是一种默认的行为,它确保了类加载的有序性和安全性。然而,在某些特殊情况下,开发者可能需要打破这种机制来实现特定的功能,例如热部署、插件化开发等。以下是一些打破双亲委派机制的方法:
- 自定义类加载器
最直接的方式是创建自定义的类加载器,并在其加载类的过程中不遵循双亲委派机制。这可以通过在自定义类加载器的loadClass方法中直接加载类,而不是先调用父类加载器的loadClass方法来实现。
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 自定义加载逻辑,不调用super.loadClass(name)
// ...
}
}
然而,这样做可能会破坏Java的安全模型,因为它允许自定义类加载器加载Java核心库中的类,这可能会导致安全问题。
- 使用线程上下文类加载器
在Java中,每个线程都有一个与之关联的上下文类加载器(ContextClassLoader)。这个类加载器可以通过Thread.currentThread().getContextClassLoader()来获取。线程上下文类加载器为Java应用程序提供了一种在运行时动态加载类的方式,而不必受双亲委派机制的限制。
Thread.currentThread().setContextClassLoader(new CustomClassLoader());
然后,可以使用这个上下文类加载器来加载类,而不是使用默认的类加载器。
- 使用Java的代理类加载器
在某些情况下,可以使用Java的代理类加载器(如URLClassLoader)来加载类,这些类加载器提供了更多的灵活性来加载类。虽然它们通常遵循双亲委派机制,但可以在必要时被修改或扩展来打破这个机制。
初始之外还有其他的方式比如:使用Java 9的模块化系统(JPMS),但它提供了一种新的方式来管理类的加载和隔离。 ; 或者使用使用OSGi(Open Service Gateway initiative)它提供了自己的类加载器机制,允许不同的模块(bundle)独立地加载和管理类,这个一般我们接触的较少。
需要注意的是,打破双亲委派机制可能会导致类加载和安全性方面的问题。因此,在决定这样做之前,应该仔细考虑其潜在的影响,并确保采取了适当的措施来确保系统的稳定性和安全性。
我们熟知的Tomcat就打破了双亲委派机制,它通过自定义类加载器的方式来实现APP应用加载隔离具体的内容请看《深入源码剖析Tomcat如何打破双亲委派》
5.类加载流程
JVM(Java虚拟机)的类加载流程主要包括:加载,验证,准备,解析,初始化 几个阶段:
类的加载、验证、准备、解析和初始化这五个阶段通常被称为类的链接(Linking)过程。
-
加载(Loading):
当
使用到某个类时
,JVM会通过类的全限定名查找和加载class文件,并通过IO读入字节码文件,将其加载到JVM中,在方法区(元空间)会存储好class,而在堆内存中会生成一个代表这个类的java.lang.Class对象
,作为方法区这个类的各种数据的访问入口。 -
验证(Verification):
确保被加载的类的正确性和安全性
。包括文件格式验证、元数据验证、字节码验证和符号引用验证。 -
准备(Preparation):
为类的
静态变量分配内存,并将其初始化为默认值,比如 static int a = 1 , 在这里会赋初始值 0
。这里不包括用final
修饰的静态变量,因为final在编译的时候就会分配了,准备阶段会显式赋值 -
解析(Resolution):
把类中的
符号引用转换为直接引用
。这主要是将类名、字段名、方法名等符号引用转换为指向方法区中的实际内存地址的直接引用,这个过程叫:静态链接
,而动态链接
是在程序运行期间完成的将符号引用替换为直接引用 -
初始化(Initialization) :
为类的
静态变量赋予正确的初始值,也就是第三步准备阶段的今天变量赋正确的初始值
(如果有的话)。这个阶段会执行类构造器<clinit>
()方法,这是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static块)中的语句合并产生的。 -
使用(Using):
类的初始化完成后,就可以通过实例化类、调用类的静态方法等方式来使用这个类了。
-
卸载(Unloading):
当类的生命周期结束时,JVM的垃圾回收机制会
回收类的内存
,这个过程称为类的卸载。但在Java中,类的卸载是由JVM来控制的,开发者通常不需要显式地卸载类。
文章对你有帮助请给个好评,下一章:JVM优化-深入JVM内存模型