文章目录
类加载时机
什么时候开始加载,虚拟机规范并没有强制性的约束,对于其它大部分阶段究竟何时开始虚拟机规范也都没有进行规范,这些都是交由虚拟机的具体实现来把握。所以不同的虚拟机它们开始的时机可能是不同的。但是对于初始化却严格的规定了有且只有四种情况必须先对类进行“初始化”(加载,验证,准备自然需要在初始化之前完成):
- 1:遇到 new 、 getstatic 、 putstatic 和 invokestatic 这四条指令时,如果对应的类没有初始化,则要对对应的类先进行初始化。
这四个指令对应到我们java代码中的场景分别是:- new关键字实例化对象的时候;
- 读取或设置一个类的静态字段(读取被final修饰,已在编译器把结果放入常量池的静态字段除外) ;
- 调用类的静态方法时。
- 2: 使用 java.lang.reflect 包方法时对类进行反射调用的时候。
- 3:初始化一个类的时候发现其父类还没初始化,要先初始化其父类。
- 4: 当虚拟机开始启动时,用户需要指定一个主类,虚拟机会先执行这个主类的初始化。
类加载器
- 启动类加载器(Bootstrap ClassLoader):
- 负责加载 JAVA_HOME\lib 目录中的
- 或通过-Xbootclasspath参数指定路径中的
- 且被虚拟机认可(按文件名识别,如rt.jar)的类
- 由C++实现,不是ClassLoader子类
- 扩展类加载器(Extension ClassLoader):
- 负责加载 JAVA_HOME\lib\ext 目录中的,
- 或通过java.ext.dirs系统变量指定路径中的类库
- 应用程序类加载器(Application ClassLoader):
- 负责加载用户路径(classpath)上的类库。
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrapClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
自定义类加载器
自定义类加载器步骤
- 1:继承ClassLoader
- 2:重写findClass()方法
- 3:调用defineClass()方法
实践
下面写一个自定义类加载器:指定类加载路径在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:自定义类加载器,代码如下:
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@4e25154f
自定义类加载器的作用
- JVM自带的三个加载器只能加载指定路径下的类字节码。
- 如果某个情况下,我们需要加载应用程序之外的类文件呢?比如本地D盘下的,或者去加载网络上的某个 类文件,这种情况就可以使用自定义加载器了
双亲委派模型
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
- 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器
- 只有当父类加载器无法完成加载任务时,才会尝试执行加载任务
采用双亲委派的一个好处是:
- 比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
为什么要使用双亲委托这种模型呢?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
- 考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用 户自定的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索 类的默认算法。
但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
- 只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时。
- 比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的
业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader
破坏双亲委派模型
为什么需要破坏双亲委派?
因为在某些情况下父类加载器需要加载的class文件由于受到加载范围的限制,父类加载器无法加载到需要的文件,这个时候就需要委托子类加载器进行加载。
而按照双亲委派模式的话,是子类委托父类加载器去加载class文件。这个时候需要破坏双亲委派模式才能加载成功父类加载器需要的类。也就是说父类会委托子类去加载它需要的class文件。
以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了 MySQL Connector ,这些实现类都是以jar包的形式放到classpath目录下。
那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类(classpath下),然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.junit.Test;
public class TestJdbc {
@Test
public void testJdbc() {
Connection connection = null;
PreparedStatement preparedStatement = null; ResultSet rs = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection = DriverManager connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/ssm? characterEncoding=utf-8", "root", "root");
// 定义sql语句 ?表示占位符
String sql = "select * from user where id = ?";
// 获取预处理 statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第一个参数为 sql 语句中参数的序号(从 1 开始),第二个参数为 设置的
preparedStatement.setInt(1, 1);
// 向数据库发出 sql 执行查询,查询出结果集
rs = preparedStatement.executeQuery();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
}catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
}catch (SQLException e) {
}
}
}
}
}