当Java程序运行时,Java虚拟机会根据类的全限定名查找对应的.class文件并加载到内存中。加载过程包括读取字节码文件、解析文件结构、创建java.lang.Class对象等。加载之后,Java虚拟机会对类进行链接(验证、准备和解析)、初始化(执行静态字段和静态代码块)等操作,最终将类的实例化、方法调用等操作映射到操作系统的执行过程。
Java的.class文件是由Java编译器(如javac)将源代码编译后生成的字节码文件。它是一种平台无关的二进制格式,包含了Java程序的类、方法、变量等信息。当程序运行时,Java虚拟机(JVM)会加载.class文件并对其进行解释或编译成本地机器指令来执行。
任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)
Class文件是一组以8个字节为基础单位的二进制流 ,其结构遵循Java虚拟机规范,主要组成部分如下:
Java类在Java虚拟机(JVM)中的生命周期涵盖了以下几个阶段:加载(Loading)、链接(Linking)、初始化(Initialization)和卸载(Unloading)。下面是对每个阶段的详细介绍:
以上就是Java类在JVM中的生命周期。值得注意的是,类的生命周期与对象的生命周期是不同的,对象的生命周期主要关注对象的创建、使用和垃圾收集。
Java类加载器(ClassLoader)在Java运行时环境中负责将字节码文件(.class)加载到内存中,为类的实例化、方法调用等提供支持。在Java中,类加载器是以java.lang.ClassLoader类的形式实现的。以下是Java中常见的几种类加载器:
Java类加载器遵循双亲委派模型(Parent Delegation Model)。当一个类需要被加载时,会先交由其父类加载器尝试加载,直到引导类加载器。若父类加载器无法加载该类,则会由当前类加载器尝试加载。这个模型可以避免类的重复加载,保证Java核心类库的安全。
classloader的双亲委托机制是指多个类加载器之间存在父子关系的时候,某个class类具体由哪个加载器进行加载的问题。
其具体的过程表现为:
双亲委托机制的作用:避免类重复加载导致冲突,保证Java核心库的安全。
例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
Bootstrap ClassLoader负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。
Extension ClassLoader是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。我们就可以将我们自己的包放在以上目录下,就会自动加载进来了。
Application Classloader 由sun.misc.Launcher$AppClassLoader来实现,ClassLoader类中的getSystemClassLoader()方法的返回值就是应用程序类加载器,它负责加载用户类路径(ClassPath)上所有的类库。
用户路径:环境变量classpath或者系统属性java.class.path
我们可以自定义类加载器,满足特殊的类加载需求,如解决类冲突,实现热加载,实现jar包的加密保护。
可以通过两种方式实现类加载器
如果没有太复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法,具体可参考AppClassLoader和ExtClassLoader。
Java提供了java.lang.ClassLoader抽象类,你可以通过继承这个类并重写findClass方法来实现自定义类加载器。
以下是一个简单的自定义类加载器的示例:
java复制代码public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
if (b == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, b, 0, b.length);
}
}
private byte[] loadClassData(String name) {
// implement your code to load class data
// this can be read from a file, network, or any other source
return null;
}
}
在上面的示例中,findClass方法是当父类加载器找不到类时会被调用的方法。在这个方法中,你需要实现从特定源加载类数据的逻辑。加载的类数据应以字节码形式返回,然后通过defineClass方法将这些字节码转换为Class对象。
参数String name通常表示类的全限定名(Fully Qualified Class Name, FQCN)。全限定名包括类所在的包名和类名,例如java.lang.String。
URLClassLoader是Java提供的一个类加载器,它可以从指定的URL列表加载类和资源。如果你的类文件位于某个URL可访问的位置(例如网络位置或本地文件系统),你可以通过继承URLClassLoader来创建自定义类加载器。
以下是一个简单的示例:
scala复制代码import java.net.URL;
import java.net.URLClassLoader;
public class MyURLClassLoader extends URLClassLoader {
public MyURLClassLoader(URL[] urls) {
super(urls);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
}
在这个示例中,MyURLClassLoader继承了URLClassLoader。我们传递一个URL数组到超类的构造器,这些URL指示类加载器从哪些位置加载类和资源。
然后,我们覆盖了findClass方法。在这个例子中,我们只是简单地调用了超类的findClass方法,但如果需要,你可以添加自定义的类加载逻辑。
以下是如何使用这个自定义类加载器的示例:
ini复制代码URL url = new File("./myClasses/").toURI().toURL(); // or any valid URL
URL[] urls = new URL[]{url};
MyURLClassLoader myLoader = new MyURLClassLoader(urls);
Class<?> myClass = myLoader.loadClass("com.example.MyClass");
在这个示例中,我们创建了一个新的MyURLClassLoader实例,指定了类文件的位置(在这个例子中,类文件位于本地文件系统的./myClasses/目录,你可以替换为任何有效的URL)。然后,我们使用loadClass方法加载了一个名为"com.example.MyClass"的类。
**反向委派(Inverse of Delegation)**是Java类加载器中使用的一种特殊的加载策略。在Java的标准类加载机制(即双亲委派模型)中,类加载器在加载类时首先会委派给父类加载器,只有当父类加载器无法加载时才自己加载。但在反向委派中,类加载器会尝试首先加载类,只有当自己无法加载时才委派给父类加载器。
这种策略在某些情况下是必要的,例如在容器(如Tomcat)中,容器需要使用自己的类加载器来加载应用,以防止应用的类和容器的类发生冲突。
线程上下文类加载器是一个指向类加载器的引用,具体使用哪种类加载策略由指向的类加载器决定。
默认情况下,线程会继承其父线程的上下文类加载器。可以通过Thread.getContextClassLoader()方法获取线程的上下文类加载器,也可以通过Thread.setContextClassLoader(ClassLoader cl)方法设置线程的上下文类加载器。
由于线程上下文类加载器可以被设置为任何一个类加载器,包括使用反向委派策略的类加载器,因此,通过线程上下文类加载器,可以实现反向委派(Inverse of Delegation)策略。例如,在Java的一些Web容器(如Tomcat)中,每个Web应用有自己的类加载器,这个类加载器通常使用的是反向委派策略。
SPI(Service Provider Interface)是Java提供的,允许第三方提供具体实现的一种服务发现机制。它允许核心库(或者框架)的代码调用由第三方实现的接口,而不需要核心库代码对这些实现有任何了解。这种机制使得核心库(或者框架)的功能可以被扩展或替换。
在Java的类加载模型中,类加载器有一个重要的特性,那就是双亲委派模型。这个模型的基本原则是,当一个类加载器需要加载一个类时,它首先会将这个任务委派给它的父类加载器,只有当父类加载器无法完成这个任务时,它才会尝试自己去加载。这种模型可以确保Java核心库的类型安全,但它也带来了一个问题,那就是父类加载器不能访问子类加载器加载的类。
然而,在SPI机制中,我们通常希望系统的核心库代码(如JDBC、JNDI等)能够调用并使用由应用级类加载器加载的第三方实现类。这就需要一种机制来克服双亲委派模型的限制,而线程上下文类加载器就是这种机制的实现。
线程上下文类加载器的设计目标就是为了成为一个能够被父类加载器请求,来加载特定资源或者类的加载器。当SPI接口被系统类库加载时,Java会使用当前线程的上下文类加载器,来加载对应的实现类。这样,即使SPI接口是由父类加载器加载的,它也能找到并加载由应用级类加载器加载的实现类。
例如,当JDBC驱动程序是由应用级类加载器加载的,而JDBC类库是由系统类加载器加载的时候,JDBC类库代码可以通过线程上下文类加载器,来加载和使用应用级类加载器加载的JDBC驱动程序。
通过线程上下文类加载器,Java的SPI机制可以正常工作,即使在使用双亲委派模型的类加载环境中。
自定义类加载器的一些应用场景:
问题场景:
某个服务引用了消息中间件和微服务中间件,这两个中间件也同时引用了fastjson-2.0和fastjson-3.0版本,而业务自己本身也引用了fastjson-1.0版本。这三个版本表现不同之处在于classA类中方法数目不相同,我们根据maven依赖处理的机制,引用路径最短的fastjson-1.0会真正作为应用最终的依赖,其它两个版本的fastjson则会被忽略,那么中间件在调用method2()方法的时候,则会抛出方法找不到异常。
解决方案是使用不同的类加载器进行fastjson的加载。
以下是一个简单的例子,我们先创建一个自定义类加载器
java复制代码public class CustomClassLoader extends ClassLoader {
private String classpath;
public CustomClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadClassData(name);
return this.defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
private byte[] loadClassData(String classname) throws IOException {
String path = classpath + classname.replace('.', '/') + ".class";
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
}
}
然后,您可以为每个中间件和业务代码创建一个CustomClassLoader实例,指定它们各自的类路径,例如:
java复制代码CustomClassLoader dubboClassLoader = new CustomClassLoader("/path/to/dubbo/classes");
CustomClassLoader metaqClassLoader = new CustomClassLoader("/path/to/metaq/classes");
CustomClassLoader businessClassLoader = new CustomClassLoader("/path/to/business/classes");
然后,您可以使用这些类加载器来加载和执行对应的代码,例如:
java复制代码Class<?> dubboClass = dubboClassLoader.loadClass("com.example.DubboClass");
Object dubboInstance = dubboClass.newInstance();
Method dubboMethod = dubboClass.getMethod("methodToInvoke");
dubboMethod.invoke(dubboInstance);
同样的,对于消息中间件和业务代码也可以这样做。
通常一个项目的代码由以上四部分组成,即基础类、扩展类、二方包/三方包、以及我们自己编写的业务代码组成。上面的一排是我们通常的类加载结构,其中业务代码和二方包/三方包是由应用加载器加载的。而实际开发和调试的过程中,主要变化的是业务代码,并且业务代码相对二方包/三方包的内容来说会更少一些。因此我们可以将业务代码单独通过一个自定义的加载器Custom Classloader来进行加载,当监控发现业务代码发生改变后,我们重新加载启动,老的业务代码的相关类则由虚拟机的垃圾回收机制来自动回收。其工程流程大概如下:
RestartClassLoader为自定义的类加载器,其核心是loadClass的加载方式,我们发现其通过修改了双亲委托机制,默认优先从自己加载,如果自己没有加载到,从从parent进行加载。这样保证了业务代码可以优先被RestartClassLoader加载。进而通过重新加载RestartClassLoader即可完成应用代码部分的重新加载。