验证阶段确保加载的类的信息符合JVM规范,不会对JVM造成危害。这包括对字节码的结构、常量池中的索引、数据类型等进行检查。
符号引用验证无法通过虚拟机会抛出java.lang.IncompatibleClassChangeError的子类,如Java.lang.IllegalAccessError\java.lang.NoSuchFieldError\java.lang.NoSuchMethodError等
准备阶段为类的静态变量分配内存并在方法区中设置默认值,但并不执行任何代码,如静态初始化块(static {}
)不会被执行。
public static int value = 123;
上述代码在准备过后初始值是0,value赋值123的动作在类初始化时才执行
但是如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段就会被初始化
public static final int value = 123;
上述代码准备阶段过后value的值就是123
解析阶段将常量池中的符号引用替换为直接引用,直接引用不在使用编号二十使用内存地址访问数据
eg:
public class HsdbDemo {
public static int i=123;
public static void main(String[] args) throws IOException {
new HsdbDemo();
System.in.read();
}
}
这个类的字节码文件中,父类索引了一个常量池中的数据
然后我们启动这个方法,进入这个线程浏览类
可以看到这里是实际的内存地址
初始化阶段执行类构造器 <clinit>()
方法,这会执行类的静态初始化块和静态变量赋值,使类准备好供应用程序使用,clinit方法对接口或类来说不是必须的,如果没有静态语句块也没有静态变量赋值,就没有clinit方法
clinit方法是编译器自动收集类中的所有静态类变量赋值动作和静态语句块中的语句合并产生的
如下的类中
public class Test {
static int i=1;
static {
i = 0;
}
}
生成的字节码文件中会有一个clinit方法
静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值,但是不能访问,如果强行访问就会报非法向前引用的错误
clinit中执行静态代码块和给静态变量赋值的顺序是按照我们代码的顺序来的,代码中先给静态变量赋值,那么clinit就会先给静态变量赋值,要是代码先执行了静态代码块,clinit就会先执行静态代码块
如下,先给静态变量赋值
public class Test {
static int i=1;
static {
i = 0;
}
}
上述类的clinit方法的字节码指令如下
执行完静态变量值为0
再来看执行静态代码块的情况
public class Test {
static {
i = 0;
}
static int i=1;
}
上述类的clinit方法的字节码指令如下
执行完静态变量值为1
clinit方法与构造函数不同,不需要显示调用父类构造器,java虚拟机会保证子类clinit方法执行前,父类clinit方法已经执行完毕,因此java虚拟机执行的第一个clinit方法的类型肯定是java.lang.Object
因此父类静态语句块执行优先于子类变量的赋值操作,下述代码输出结果为2
public class Test {
static class Parent{
public static int A = 1;
{
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args){
System.out.println(Sub.B);
}
}
接口不能使用静态代码块,但是仍然有静态变量赋值操作,所以还是会有clinit方法,但是接口的clinit方法不需要先执行父接口的clinit方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化,接口的实现类在初始化时,也不会执行接口的clinit方法
如果有多个线程同时初始化一个类,此时只有一个线程会执行这个类的clinit方法,其他线程都要阻塞等待,直到clinit方法执行完毕
如下代码,static代码中执行while循环,其他线程就会阻塞
public class DeadLoopClassTest {
static class DeadLoopClass{
static {
if(true){
System.out.println(Thread.currentThread().getName()+"执行clinit");
while(true){
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Runnable script = () -> {
System.out.println(Thread.currentThread().getName()+" 开始");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread().getName()+" 结束");
};
Thread th1 = new Thread(script);
th1.setName("任务1");
Thread th2 = new Thread(script);
th2.setName("任务2");
th1.start();
th2.start();
while(true){
System.out.println("任务1状态 "+th1.getState());
System.out.println("任务2状态 "+th2.getState());
Thread.sleep(3000);
}
}
}
对于静态代码块而言,你不需要担心显式的同步问题,因为JVM已经处理了这些细节。但是,如果你在静态代码块中访问了非静态成员或进行了其他可能引起并发问题的操作,那么你可能需要考虑使用synchronized关键字或其他同步机制来保护这些操作,确保线程安全
以下几种方式会导致类的初始化
访问一个类的静态变量或者而静态方法,注意变量是final修饰并且等号右边不会触发初始化,因为准备阶段就已经初始化了
下述代码中,访问了静态变量,执行了初始化
public class HelloWorld {
public static void main(String[] args){
System.out.println(TestStatic.i);
}
}
class TestStatic{
static{
System.out.println("初始化...");
}
public static int i=0;
}
结果为
然后把静态变量用final修饰
public class HelloWorld {
public static void main(String[] args){
System.out.println(TestStatic.i);
}
}
class TestStatic{
static{
System.out.println("初始化...");
}
public static final int i=0;
}
输出结果为
调用Class.forName(String className)
public class HelloWorld {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("TestStatic");
}
}
class TestStatic{
static{
System.out.println("初始化...");
}
public static final int i=0;
}
输出
new一个对象
public class HelloWorld {
public static void main(String[] args) throws ClassNotFoundException {
TestStatic testStatic = new TestStatic();
}
}
class TestStatic{
static{
System.out.println("初始化...");
}
public static final int i=0;
}
输出
执行main方法的当前类
public class HelloWorld {
static{
System.out.println("main初始化");
}
public static void main(String[] args) throws ClassNotFoundException {
}
}
class TestStatic{
static{
System.out.println("初始化...");
}
public static final int i=0;
}
输出
类加载器是Java虚拟机提供给应用程序去获取类和接口字节码数据的技术,通过一个类得全限定名来获取描述该类得二进制字节流
每一个类加载器都有一个独立的类名称空间,比较两个类是否相等,只有在这两个类是由同一个同一个类加载器加载得前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个java虚拟机加载,只要加载他们的类加载器不同,拿这两个类必定不相等
这里的相等,包括代表类的Class对象的equals()方法,isAssignableFrom()方法,instance()方法的返回结果
参考下述代码演示了不同类加载器对instanceof 运算结果的影响
package jvm;
import java.io.IOException;
import java.io.InputStream;
/**
* @author Swimming Dragon
* @description: TODO
* @date 2024年08月12日 10:32
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null){
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof jvm.ClassLoaderTest);
}
}
这里输出结果如下
类加载器分为两个大类
负责加载存放在<JAVA_HOME>\lib目录或者被-Xbootclasspath参数所指定的路径中存放类库,如rt.jar,tools.jar等
启动类加载器在虚拟机中实现的,代码中无法获取到,class类源码中的getClassLoader方法中也说明了启动类加载器加载的类调用这个方法会返回null
如下代码所示
public class BootstrapClassLoaderDemo {
public static void main(String[] args){
System.out.println(String.class.getClassLoader());
}
}
输出结果为
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码形式实现的,负责加载<JAVA_HOME>\lib\ext目录或者被java.ext.dirs系统变量指定的路径中的类库,使用-Djava.ext.dirs=目录,这种方式会覆盖原始目录,因此需要指定原始目录和自定义目录,eg:-Djava.ext.dirs=<JAVA_HOME>\lib\ext;自定义目录;
这个类加载器是在类sun.misc.Launcher$AppClassLoader中以Java代码形式实现的,负责加载用户类路径上的所有的类库
双亲委派模型要求除了顶层得启动类加载器外,其余的类加载器都有自己的父类加载器
这里的父子关系并不是通过继承类声明的,而是在类加载器中通过字段属性指定了父类加载器
如果一个类加载器收到类加载的请求,检查自己是否已经加载过,没有的话他首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层得启动类加载器中,只有当父加载器反馈自己无法完成这个请求时,子加载器才会尝试自己去完成加载
自定义类加载器中,可以直接重写loadClass()方法,取消loadClass()方法中调用父类加载器加载的过程,直接自己加载,以此来直接打破双亲委派机制
除此之外,可以重写findClass方法而不是去重写loadClass方法,这样既可以按自己的意愿去加载类,也可以保证自定义的类加载器符合双亲委派规则
双亲委派模型解决了各个类加载器协作时基础类型一致得问题,基础类型经常被用户代码继承、调用,但在某些情况下基础类型也需要回调用户的代码
典型的例子就是JNDI服务,他的代码由启动类加载器来完成加载,但是他需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,但是这些代码往往是由AppClassLoader加载的,启动类无法加载这些代码
线程上下文类加载器(Thread Context ClassLoader)通过java.lang.Thread的setContextClassLoader()方法进行设置,如果创建线程时未设置,他会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,这个类加载器默认就是应用程序类加载器,如下图所示
有了线程上下文加载器,JNDI服务就可以使用这个加载器去加载SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,Java中涉及SPI的加载基本上都是通过这种方式完成,例如JNDI、JDBC等
当SPI服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的方式,JDK6提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,才算是给SPI的加载提供了一种相对合理的解决方案
OSGi实现模块化热部署的关键是他自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉
当收到类加载请求时,OSGi按照下面的顺序进行搜索:
java.*
中,那么会交由父级类加载器(通常是启动类加载器Bootstrap ClassLoader)代理完成 。org.osgi.framework.bootdelegation
)中定义,那么也会交由父级代理完成 。Import-Package
中声明的包,或者是在先前的加载过程中动态导入的,那么请求将转发到导出该包的bundle的类加载器 。Require-Bundle
导入的包中,那么请求将按照在bundle的manifest中指定的顺序委派给其他bundle的类加载器 。DynamicImport-Package
导入的包中,则会尝试进行动态导入。如果找到了合适的导出者,并且建立了连接,则请求将交由导出该包的bundle的类加载器处理 。Java模块化将Java应用程序划分为一组模块,每个模块具有自己的边界和依赖关系。模块化使得开发人员能够更好地组织和管理复杂的Java项目,并提供更好的封装和可重用性
JDK9中引入了Java模块化系统(Java Platform Module System,JPMS),Java的模块定义包含以下内容:
Java模块化的核心原理是将应用程序划分为一组模块,并通过 module-info.java
文件来定义模块的信息和依赖关系。module-info.java
文件位于模块的根目录下,它使用 module
关键字声明模块的名称,并通过 requires
和 exports
关键字定义模块的依赖关系和导出的包。通过这种方式,可以清晰地定义模块之间的依赖关系,避免类路径冲突,并提供更好的封装性和可重用性 。
在JDK9之前,如果类路径中缺失了运行时依赖的类型,那就只能等程序运行到发生该类型的加载、链接时才会爆出异常,在JDK9之后,如果启用了模块化进行封装,模块就可以声明对其他模块的显示依赖,这样Java虚拟机就能在启动时验证应用程序开发阶段设定好的依赖关系在运行期是否完备,如果缺失就直接启动失败
可配置的封装隔离机制还解决了原来类路径上跨jar文件的public类型的可访问性问题
模块化系统在jdk9引入,为了支持老版本的程序也可以访问到标准类库模块中导出的包,模块化系统按照以下规则加载
java --list-modules可以查看模块列表
扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代,取消了<JAVA_HOME>\lib\ext目录;
新版本jdk中取消了<JAVA_HOME>\jre目录,可以使用jlink命令组合构建程序运行所需要的JRE,在jdk9中通过jlink实现,例如:
jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.sql,java.xml --output my-custom-jre
模块化后的jdk安装目录如下有个jmods文件夹
下面存放的就是jdk的默认71个模块
平台类加载器和应用程序类加载器都不在派生自java.net.URLClassLoader,现在启动类加载器、平台类加载器、应用程序类加载器全部继承于jdk.internal.loader.BuiltinClassLoader
jdk9中任然位处三层类加载器和双亲委派的架构,但类加载器的委派关系也发生了变动,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到一个系统的模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载