jvm-类加载时机、类加载器、双亲委派模型、破坏双亲委派模型

类加载时机

什么时候开始加载,虚拟机规范并没有强制性的约束,对于其它大部分阶段究竟何时开始虚拟机规范也都没有进行规范,这些都是交由虚拟机的具体实现来把握。所以不同的虚拟机它们开始的时机可能是不同的。但是对于初始化却严格的规定了有且只有四种情况必须先对类进行“初始化”(加载,验证,准备自然需要在初始化之前完成):

  • 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(11);
			// 向数据库发出 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) {
				}
			}
		}
	}
}
		
		
		
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值