在这篇专栏文章中,我们将深入探讨Java虚拟机(JVM)的奥秘。JVM是Java语言的核心组件,负责将Java字节码转换为特定计算机硬件能理解的本地机器代码。通过深入了解JVM,我们可以更好地理解Java应用程序的运行机制和性能调优,为Java开发者提供有价值的指导。通过本专栏的学习,您将掌握JVM的核心原理和优化技巧,从而成为一名更加出色的Java开发者。让我们一起,走进JVM的世界,探索其深邃奥妙!
当我们使用java命令启动Java应用程序时,例如java MyApp,实际上是执行了JVM中的main函数。main函数位于JVM的启动模块,它首先使用引导类加载器加载核心类库,然后调用Launcher类的main方法启动Java应用程序。
Launcher类位于sun.misc包中,是Java应用程序启动器的核心组件。它负责加载和启动Java应用程序,以及设置类加载器(如系统类加载器和扩展类加载器)。
在Launcher构造方法中,确实创建了两个类加载器:sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。当JVM启动时,它会创建一个Launcher实例,这个实例负责配置和初始化类加载器。在构造方法中,首先创建扩展类加载器,然后创建应用类加载器。应用类加载器的父加载器被设置为扩展类加载器,这意味着在加载类时,应用类加载器会首先尝试使用扩展类加载器加载类。
如下源码:
//Launcher的构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构造扩展类加载器,在构造的过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
//Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
//省略一些不需关注代码
}
这段代码是一个构造方法,用于创建一个名为Launcher的对象。Launcher类的主要目的是创建两个类加载器:一个扩展类加载器(ExtClassLoader)和一个应用类加载器(AppClassLoader)。这两个类加载器用于加载Java程序的类。
总之,这个构造方法用于创建一个Launcher对象,它负责创建和配置两个类加载器(扩展类加载器和应用类加载器)。这些类加载器用于加载Java程序的类。在创建过程中,应用类加载器的父加载器被设置为扩展类加载器,以确保按照正确的顺序加载类。
Launcher类的主要作用有以下几点:
以下是详细的类加载器初始化过程:
JDK为我们提供了以下三种主要的类加载器:
除了这三种主要的类加载器,JDK还允许我们自定义类加载器。自定义类加载器通常需要继承 java.lang.ClassLoader类,重写findClass方法以实现特定的类加载逻辑。自定义类加载器可以用于实现热部署、隔离不同应用程序的类加载等场景
双亲委派机制(Parent Delegation Model)是Java类加载器在加载类时遵循的一种工作原则。这种机制可以确保Java类的唯一性和安全性。双亲委派机制的核心思想是:当一个类加载器收到类加载请求时,它首先不会自己尝试加载这个类,而是将请求委派给其父类加载器。这个过程会一直向上委派,直到达到引导类加载器。只有当父类加载器无法完成类加载请求时,当前类加载器才会尝试自己加载该类。
双亲委派机制的具体工作过程如下:
我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查当前类加载器是否已经加载了该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //如果当前加载器父加载器不为空则委托父加载器加载该类
c = parent.loadClass(name, false);
} else { //如果当前加载器父加载器为空则委托引导类加载器加载该类
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();
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
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;
}
}
这段代码展示了ClassLoader类中的loadClass方法。该方法负责根据给定的类名加载类,并实现了双亲委派机制。双亲委派机制是Java类加载器中的一种策略,用于确保类按照正确的顺序被加载,避免重复加载以及安全问题。
方法loadClass的工作原理如下:
双亲委派机制的优点包括:
需要注意的是,双亲委派机制并不是强制性的。自定义类加载器可以选择不遵循双亲委派机制,根据实际需求实现特定的类加载逻辑。但在大多数情况下,遵循双亲委派机制是有益的。
Java类加载过程可以分为五个阶段:加载、验证、准备、解析和初始化。下面我们详细描述每个阶段的工作内容:
类加载完成后,JVM会将其放入方法区,并在堆内存中创建一个java.lang.Class对象,用于表示该类的元数据信息。接下来,JVM通过执行引擎执行该类的字节码,实现程序的运行。
在整个类加载运行过程中,JVM采用了双亲委派模型来保证类的唯一性。当类加载器收到类加载请求时,会先将请求委托给父类加载器,直至委派到根类加载器;若父类加载器无法加载该类,则尝试由当前类加载器加载。这种模式避免了类的重复加载,同时保证了Java核心类库的安全性。
自定义类加载器主要包括以下几个步骤:
1、继承java.lang.ClassLoader类:自定义类加载器需要继承ClassLoader类,这是Java中所有类加载器的基类。
public class MyClassLoader extends ClassLoader {
// ...
}
2、重写findClass方法:findClass方法是ClassLoader类中的一个受保护方法,用于在类加载器的类路径下查找并加载指定的类。自定义类加载器需要重写这个方法以实现特定的类加载逻辑。在findClass方法中,可以使用defineClass方法将字节码转换为Class对象。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
3、实现类数据的加载:在上面的示例中,我们使用了一个名为getClassData的方法来获取类的字节码数据。这个方法需要根据实际需求进行实现。例如,可以从文件系统、网络、数据库或其他来源加载类数据。
private byte[] getClassData(String className) {
// 实现类数据的加载,例如从文件系统、网络、数据库等来源加载类数据
// ...
}
4、使用自定义类加载器:创建自定义类加载器的实例,并使用loadClass方法加载类。
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader classLoader = new MyClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.MyClass");
Object instance = clazz.newInstance();
}
以上示例展示了如何创建一个简单的自定义类加载器。实际上,自定义类加载器可以根据需求实现更复杂的逻辑,例如实现热部署、隔离不同应用程序的类加载等。
要实现打破双亲委派机制,可以在自定义类加载器中重写loadClass方法,改变类加载的顺序。以下是一个示例:
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 首先检查类是否已经加载
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 尝试使用自定义类加载器加载类
try {
clazz = findClass(name);
} catch (ClassNotFoundException e) {
// 忽略异常,表示自定义类加载器无法加载该类
}
// 如果自定义类加载器无法加载类,委派给父类加载器
if (clazz == null) {
clazz = super.loadClass(name);
}
return clazz;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] getClassData(String className) {
// 实现类数据的加载,例如从文件系统、网络、数据库等来源加载类数据
// ...
}
}
在上面的示例中,我们重写了loadClass方法,首先尝试使用自定义类加载器加载类,如果无法加载,再委派给父类加载器。这样,我们就实现了打破双亲委派机制的自定义类加载器。
需要注意的是,打破双亲委派机制可能会导致类的重复加载以及安全问题。因此,在实现自定义类加载器时,应该根据实际需求和场景权衡利弊。
作为Web容器的Tomcat需要解决的一些关键问题:
通过解决这些问题,Tomcat作为一个Web容器能够提供良好的应用程序部署环境,确保类库之间的隔离、共享、容器与应用程序之间的隔离以及对JSP文件的动态修改支持。这些特性使得Tomcat能够满足多样化的应用程序需求,并确保在部署和运行Web应用程序时的稳定性和安全性。
以下这张图是Tomcat类加载器之间的关系:
以下是一个简要的总结:
Tomcat在某种程度上打破了双亲委派模型以实现Web应用程序之间的隔离性。在标准的双亲委派模型中,类加载器首先会委托给其父加载器来加载类,只有在父加载器无法找到类时,当前类加载器才会尝试加载该类。
然而,在Tomcat中,WebappClassLoader采用了一种被称为"child first"(子优先)策略。在这种策略下,WebappClassLoader首先尝试加载其自己的目录下的类(如WEB-INF/lib和WEB-INF/classes目录),而不是直接委托给父加载器。这样做的原因是为了确保Web应用程序能够使用其自己的类库版本,而不是使用父加载器提供的版本。这在处理不同Web应用程序使用不同版本的类库时非常重要,例如,不同的应用程序可能使用不同版本的Spring框架。
当WebappClassLoader无法在自己的目录下找到类时,它会将请求传递给父加载器(如SharedClassLoader或CommonClassLoader)以加载类。这样,在保持应用程序之间的隔离性的同时,仍然允许它们共享一些通用的类库。
总之,Tomcat通过修改双亲委派机制来实现Web应用程序之间的隔离。这使得每个应用程序可以使用自己的类库版本,同时仍然能够共享公共类库。虽然这与标准的双亲委派模型有所不同,但它在处理多个部署在同一容器中的Web应用程序时非常有效。
Tomcat主要使用了两个自定义类加载器:WebappClassLoader和StandardClassLoader。
以下是Tomcat如何打破双亲委派机制的示例:
在WebappClassLoader中,重写loadClass方法:
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先检查类是否已经加载
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 尝试使用WebappClassLoader加载类
try {
clazz = findClass(name);
} catch (ClassNotFoundException e) {
// 忽略异常,表示WebappClassLoader无法加载该类
}
// 如果WebappClassLoader无法加载类,委派给父类加载器
if (clazz == null) {
clazz = getParent().loadClass(name);
}
return clazz;
}
在上面的示例中,WebappClassLoader首先尝试加载Web应用程序的类,然后再委派给父类加载器。这样,Tomcat就实现了打破双亲委派机制。
需要注意的是,Tomcat打破双亲委派机制的目的是为了实现Web应用程序之间的类库隔离,以及让Web应用程序可以使用自己的类库。在实际使用中,应该根据实际需求和场景权衡利弊。
模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离:
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
try {
c= findClass(name);
} catch (ClassNotFoundException e) {
// 忽略异常,表示WebappClassLoader无法加载该类
}
//非自定义的类还是走双亲委派加载
if (c == null){
c = this.getParent().loadClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class clazz = classLoader.loadClass("com.wzr.jvm.Stu1");
Object obj = clazz.newInstance();
Method method= clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader());
System.out.println();
MyClassLoader classLoader1 = new MyClassLoader("D:/test1");
Class clazz1 = classLoader1.loadClass("com.wzr.jvm.Stu1");
Object obj1 = clazz1.newInstance();
Method method1= clazz1.getDeclaredMethod("sout", null);
method1.invoke(obj1, null);
System.out.println(clazz1.getClassLoader());
}
}
这个示例代码展示了一个自定义的类加载器MyClassLoader,它继承了ClassLoader。这个类加载器可以从指定的classPath目录加载类。MyClassLoader中的loadClass方法进行了重写,这样就可以实现自定义的加载逻辑,而不是直接委派给双亲加载器。
在main方法中,创建了两个不同的MyClassLoader实例,分别指定了D:/test和D:/test1路径。然后,使用这两个类加载器加载并实例化com.wzr.jvm.Stu1类,并调用sout方法。最后,打印了每个实例的类加载器。