文章目录
一、类加载的时机
- 遇到 new 、 getstatic 、 putstatic 和 invokestatic四条字节码指令时,如果对应的类没有初始化,则要对对应的类先进行初始化。
这四个指令对应到我们java代码中的场景分别是
: new关键字实例化对象的时候
: 读取或设置一个类的静态字段(静态常量除外)
: 调用类的静态方法时 - 使用 java.lang.reflect 包方法对类进行反射调用时
- 初始化一个类的时候发现其父类还没初始化,要先初始化其父类
- 当JVM开始启动时,用户需要指定一个主类,JVM会先执行这个主类的初始化
二、类加载的过程
类加载的过程注意分为三大阶段:加载、链接、初始化。其中连接阶段又分为验证、准备、解析。
当一个类的所有实例对象都已经被回收时,类会触发卸载。
2.1 加载
加载过程主要依靠类加载器实现,目标就是将不同来源的class文件都加载到JVM内存的方法区中。到了方法区,需要将加载的信息,封装到java.lang.Class对象中。Class对象描述了这个类的构造方法、实例方法、实例变量等所有信息。
2.1.1 类和数组加载的区别
数组也有类型,称为数组类型,如:
String[] str = new String[10];
这个数组的数组类型是[Ljava.lang.String,而String是这个数组的元素类型。
- 非数组类有类加载器加载
- 数组类本身不通过类加载器创建,它由JVM直接创建,数组类的元素类型最终要靠类加载器创建。
2.2 链接
2.2.1 验证
保证二进制字节流的信息符合java虚拟机规范,并没有安全问题。
2.2.2 准备
为类变量(static修饰的字段变量或者叫静态变量)分配内存,并且为变量设置初始值
这里不包含final修饰的静态变量,因为final在编译的时候就分配了(编译的优化),同时这里也不会为实例分配初始化。
比如:
public static int x=1000;
在准备阶段x的值是0,而不是1000;
将x赋值为1000的putstatic指令是程序被编译,存放于类构造器clinit方法之中
但如果声明为
public final static int x=1000;
在编译阶段会为x生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将x赋值为1000。
2.2.3 解析
解析阶段将常量池的符号引用替换为直接引用。
解析动作主要针对接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info 、 CONSTANT_Fieldref_info 、 CONSTANT_Methodref_info 、CONSTANT_InterfaceMethodref_info四种常量类型。
2.3 初始化
初始化阶段真正开始执行类中定义的Java程序代码,完成对static修饰的类变量手动赋值,和主动调用静态代码块。
注意:
- 方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,收集顺序是自上而下的。
- 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.
- 虚拟机会保证在多线程环境中一个类的初始化方法被正确地加锁,当多条线程同时去初始化一个类时,只会有一个线程去执行该类的初始化方法,其它线程都被阻塞等待,直到活动线程初始化完毕。其他线程虽会被阻塞,只要有一个执行完,其它线程唤醒后不会再进入。同一个类加载器下,一个类型只会初始化一次。
使用静态内部类的线程安全单例实现:
public class Student {
private Student() {}
/*
* 此处使用一个内部类来维护单例 JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
*/
private static class SingletonFactory {
private static Student student = new Student();
}
/* 获取实例 */
public static Student getSingletonInstance() {
return SingletonFactory.student;
}
}
三、类加载器
不同的类加载器加载同一个class文件,会生成不同的Class对象。
启动类加载器(Bootstrap ClassLoader)
- 负责加载JAVA_HOME\lib目录中的
- 通过-Xbootclasspath参数指定的路径中被虚拟机认可的类
- 由C++实现,不是ClassLoader的子类
扩展类加载器(Extension ClassLoader)
- 负责加载JAVA_HOME\lib\ext目录
- 通过java.ext.dirs系统变量指定路径中的类库
应用程序类加载器(Application ClassLoader)
- 负责加载用户路径(classpath)上的类库
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
加载过程中会检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到Bootstrap ClassLoader逐层检查,只要某个ClassLoader已加载就视为已加载此类。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
四、自定义类加载器
4.1 步骤
- 继承ClassLoader
- 实现findClass()
- 调用defineClass()
4.2 实践
下面写一个自定义类加载器:指定类加载路径在D盘下的lib文件夹下。
(1)在本地磁盘新建一个 Test.java 类,代码如下:
package jvm.classloader;
public class Test {
public void say(){
System.out.println("Hello MyClassLoader");
}
}
(2)使用 javac -d . Test.java 命令,将生成的 Test.class 文件放到 D:/lib/jvm/classloader文件夹下。
(3)在Eclipse中自定义类加载器,代码如下:
package jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
private String classpath;
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException
{
try {
byte [] classDate=getData(name);
if(classDate==null){}
else{
//defineClass方法将字节码转化为类
return defineClass(name,classDate,0,classDate.length);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
//返回类的字节码
private byte[] getData(String className) throws IOException{
InputStream in = null;
ByteArrayOutputStream out = null;
String path=classpath + File.separatorChar +
className.replace('.',File.separatorChar)+".class";
try {
in=new FileInputStream(path);
out=new ByteArrayOutputStream();
byte[] buffer=new byte[2048];
int len=0;
while((len=in.read(buffer))!=-1){
out.write(buffer,0,len);
}
return out.toByteArray();
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
finally{
in.close();
out.close();
}
return null;
}
}
测试代码:
package jvm.classloader;
import java.lang.reflect.Method;
public class TestMyClassLoader {
public static void main(String []args) throws Exception{
//自定义类加载器的加载路径
MyClassLoader myClassLoader=new MyClassLoader("D:\\lib");
//包名+类名
Class c=myClassLoader.loadClass("jvm.classloader.Test");
if(c!=null){
Object obj=c.newInstance();
Method method=c.getMethod("say", null);
method.invoke(obj, null);
System.out.println(c.getClassLoader().toString());
}
}
}
输出层结果:
Hello MyClassLoader
jvm.classloader.MyClassLoader@4a234fe
4.3 自定义类加载器的作用
1.JVM自带的三个加载器只能加载指定路径下的类字节码。
2.如果某个情况下,我们需要加载应用程序之外的类文件呢?比如本地D盘下的,或者去加载网络上的某个类文件,这种情况就可以使用自定义加载器了
五、双亲委派模型
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
- 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器
- 只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
采用双亲委派的一个好处是:
- 比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
5.1 为什么要使用双亲委托这种模型呢?
因为这样可以避免重复加载,当父亲已经加载了该类,就没有必要子ClassLoader再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
5.2 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
5.3 既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时。
比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。