类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个 java.lang.Class 实例。一旦一个类被载入 JVM 中,同个类就不会被再次载入了。现在的问题是,怎么样才算“同一个类”?
正如一个对象有一个唯一的标识一样,一个载入 JVM 中的类也有一个唯一的标识。在 Java 中,一个类用其全限定类名(包括包名和类名)作为标识:但在 JVM 中,一个类用其全限定类名和其类加载器作为唯一标识。例如,如果在 pg 的包中有一个名为 Person 的类,被类加载器 ClassLoader 的实例 k1 负责加载,则该 Person 类对应的 Class 对象在 JVM 中表示为(Person、pg、k1)。这意味着两个类加载器加载的同名类:(Person、pg、k1)和(Person、pg、k12)是不同的,它们所加载的类也是完全不同、互不兼容的。
当 JVM 启动时,会形成由三个类加载器组成的初始类加载器层次结构。
Bootstrap ClassLoader 被称为引导(也称为原始或根)类加载器,它负责加载 Java 的核心类。在Sun 的 JVM 中,当执行 java.exe 命令时,使用 -Xbootclasspath 或 -D 选项指定 sun.boot.class.path 系统属性值可以指定加载附加的类。
JVM的类加载机制主要有如下三种。
除了可以使用 Java 提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承 ClassLoader 来实现。JVM 中这4种类加载器的层次结构如下图所示。
注意:类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系
下面程序示范了访问 JVM 的类加载器。
public class ClassLoaderPropTest { public static void main(String[] args) throws IOException { // 获取系统类加载器 ClassLoader systemLoader = ClassLoader.getSystemClassLoader(); System.out.println("系统类加载器:" + systemLoader); /* * 获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定 如果操作系统没有指定CLASSPATH环境变量,默认以当前路径作为 * 系统类加载器的加载路径 */ Enumeration<URL> em1 = systemLoader.getResources(""); while (em1.hasMoreElements()) { System.out.println(em1.nextElement()); } // 获取系统类加载器的父类加载器:得到扩展类加载器 ClassLoader extensionLader = systemLoader.getParent(); System.out.println("扩展类加载器:" + extensionLader); System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs")); System.out.println("扩展类加载器的parent: " + extensionLader.getParent()); } }
运行上面的程序,会看到如下运行结果
系统类加载器:sun.misc.Launcher$AppClassLoader@73d16e93
file:/F:/EclipseProjects/demo/bin/
扩展类加载器:sun.misc.Launcher$ExtClassLoader@15db9742
扩展类加载器的加载路径:C:/Program Files/Java/jre1.8.0_181/lib/ext;C:/Windows/Sun/Java/lib/ext
扩展类加载器的parent: null
从上面运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是null(与 Java8 有区别),但此处看到扩展类加载器的父加载器是null,并不是根类加载器。这是因为根类加载器并没有继承 ClassLoader 抽象类,所以扩展类加载器的 getParent() 方法返回null。但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是 Java 实现的。
从运行结果可以看出,系统类加载器是 AppClassLoader 的实例,扩展类加载器 ExtClassLoader 的实例。实际上,这两个类都是 URLClassLoader 类的实例。
注意:JVM 的根类加载器并不是 Java 实现的,而且由于程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回null。
类加载器加载 Class 大致要经过如下8个步骤。
其中,第5、6步允许重写 ClassLoader的 findClass() 方法来实现自己的载入策略,甚至重写 loadClass() 方法来实现自己的载入过程。
创建并使用自定义的类加载器
JVM 中除根类加载器之外的所有类加载器都是 ClassLoader 子类的实例,开发者可以通过扩展 ClassLoader 的子类,并重写该 ClassLoader 所包含的方法来实现自定义的类加载器。查阅API文档中关于 ClassLoader 的方法不难发现,ClassLoader 中包含了大量的 protected 方法——这些方法都可被子类重写。
ClassLoader 类有如下两个关键方法。
如果需要实现自定义的 ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写 findClass() 方法,而不是重写 loadClass() 方法。loadClass() 方法的执行步骤如下。
从上面步骤中可以看出,重写 findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略:如果重写 loadClass() 方法,则实现逻辑更为复杂。
在 ClassLoader 里还有一个核心方法:Class defineClass(String name, byte[] b, int off,int len) 该方法负责将指定类的字节码文件(即 Class 文件,如 Hello.class)读入字节数组 byte[] b 内,并把它转换为 Class对象,该字节码文件可以来源于文件、网络等。
defineClass() 方法管理 JVM 的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心,程序员无须重写该方法。实际上该方法是 final 的,即使想重写也没有机会。
除此之外,ClassLoader 里还包含如下一些普通方法。
下面程序开发了一个自定义的 ClassLoader,该 ClassLoader 通过重写 findClass() 方法来实现自定义的类加载机制。这个 ClassLoader 可以在加载类之前先编译该类的文件,从而实现运行 Java 之前先编译该程序的目标,这样即可通过该 ClassLoader 直接运行 Java 源文件。
public class CompileClassLoader extends ClassLoader { // 读取一个文件的内容 private byte[] getBytes(String filename) throws IOException { File file = new File(filename); long len = file.length(); byte[] raw = new byte[(int) len]; try (FileInputStream fin = new FileInputStream(file)) { // 一次读取class文件的全部二进制数据 int r = fin.read(raw); if (r != len) throw new IOException("无法读取全部文件:" + r + " != " + len); return raw; } } // 定义编译指定Java文件的方法 private boolean compile(String javaFile) throws IOException { System.out.println("CompileClassLoader:正在编译 " + javaFile + "..."); // 调用系统的javac命令 Process p = Runtime.getRuntime().exec("javac " + javaFile); try { // 其他线程都等待这个线程完成 p.waitFor(); } catch (InterruptedException ie) { System.out.println(ie); } // 获取javac线程的退出值 int ret = p.exitValue(); // 返回编译是否成功 return ret == 0; } // 重写ClassLoader的findClass方法 <strong>protected Class<?> findClass(String name) throws ClassNotFoundException</strong> { Class clazz = null; // 将包路径中的点(.)替换成斜线(/)。 String fileStub = name.replace(".", "/"); String javaFilename = fileStub + ".java"; String classFilename = fileStub + ".class"; File javaFile = new File(javaFilename); File classFile = new File(classFilename); // 当指定Java源文件存在,且class文件不存在、或者Java源文件 // 的修改时间比class文件修改时间更晚,重新编译 if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) { try { // 如果编译失败,或者该Class文件不存在 if (!compile(javaFilename) || !classFile.exists()) { throw new ClassNotFoundException("ClassNotFoundExcetpion:" + javaFilename); } } catch (IOException ex) { ex.printStackTrace(); } } // 如果class文件存在,系统负责将该文件转换成Class对象 if (classFile.exists()) { try { // 将class文件的二进制数据读入数组 byte[] raw = getBytes(classFilename); // 调用ClassLoader的defineClass方法将二进制数据转换成Class对象 clazz = defineClass(name, raw, 0, raw.length); } catch (IOException ie) { ie.printStackTrace(); } } // 如果clazz为null,表明加载失败,则抛出异常 if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } // 定义一个主方法 public static void main(String[] args) throws Exception { // 如果运行该程序时没有参数,即没有目标类 if (args.length < 1) { System.out.println("缺少目标类,请按如下格式运行Java源文件:"); System.out.println("java CompileClassLoader ClassName"); } // 第一个参数是需要运行的类 String progClass = args[0]; // 剩下的参数将作为运行目标类时的参数, // 将这些参数复制到一个新数组中 String[] progArgs = new String[args.length - 1]; System.arraycopy(args, 1, progArgs, 0, progArgs.length); CompileClassLoader ccl = new CompileClassLoader(); // 加载需要运行的类 Class<?> clazz = ccl.loadClass(progClass); // 获取需要运行的类的主方法 <strong> Method main = clazz.getMethod("main", (new String[0]).getClass()); Object[] argsArray = { progArgs }; main.invoke(null, argsArray); </strong> } }
上面程序中的粗体字代码重写了 findClass() 方法,通过重写该方法就可以实现自定义的类加载机制。在本类的 findClass() 方法中先检查需要加载类的 Class 文件是否存在,如果不存在则先编译源文件,再调用 ClassLoader 的 defineClass() 方法来加载这个 Class 文件,并生成相应的 Class 对象。
接下来可以随意提供一个简单的主类,该主类无须编译就可以使用上面的 CompileClassLoader 来运行它。
public class Hello { public static void main(String[] args) { for (String arg : args) { System.out.println("运行Hello的参数:" + arg); } } }
本示例程序提供的类加载器功能比较简单,仅仅提供了在运行之前先编译 Java 源文件的功能。实际上,使用自定义的类加载器,可以实现如下常见功能。
URLClassLoader 类
Java 为 ClassLoader 提供了一个 URLClassLoader 实现类,该类也是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类之间的继承关系)。URLClassLoader 功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。
在应用程序中可以直接使用 URLClassLoader 加载类,URLClassLoader 类提供了如下两个构造器。
一旦得到了 URLClassLoader 对象之后,就可以调用该对象的 loadClass() 方法来加载指定类。下面程序示范了如何直接从文件系统中加载 MySQL 驱动,并使用该驱动来获取数据库连接。通过这种方式来获取数据厍连接,可以无须将 MySQL 驱动添加到 CLASSPATH 环境变量中。
public class URLClassLoaderTest { private static Connection conn; // 定义一个获取数据库连接方法 public static Connection getConn(String url, String user, String pass) throws Exception { if (conn == null) { // 创建一个URL数组 <strong> URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") }; </strong> // 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader <strong>URLClassLoader myClassLoader = new URLClassLoader(urls); </strong> // 加载MySQL的JDBC驱动,并创建默认实例 <strong>Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance(); </strong> // 创建一个设置JDBC连接属性的Properties对象 Properties props = new Properties(); // 至少需要为该对象传入user和password两个属性 props.setProperty("user", user); props.setProperty("password", pass); // 调用Driver对象的connect方法来取得数据库连接 conn = driver.connect(url, props); } return conn; } public static void main(String[] args) throws Exception { System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147")); } }
上面程序中的前两行粗体字代码创建了一个 URLClassLoader 对象,该对象使用默认的父类加载器,该类加载器的类加载路径是当前路径下的 mysql-connector-java-5.1.30-bin.jar 文件,将 MySQL 驱动复制到该路径下,这样保证该 ClassLoader 可以正常加载到 com.mysql.jdbc.Driver 类。
程序的第三行粗体字代码使用 ClassLoader 的 loadClass() 加载指定类,并调用 Class 对象的 newInstance() 方法创建了一个该类的默认实例——也就是得到 com.mysql.jdbc.Driver 类的对象,当然该对象的实现类实现了 java.sql.Driver 接口,所以程序将其强制类型转换为 Driver,程序的最后一行粗体字代码通过 Driver 而不是 DriverManager 来获取数据库连接,关于 Driver 接口的用法读者可以自行查阅API文档。
正如前面所看到的,创建 URLClassLoader 时传入了一个 URL 数组参数,该 ClassLoader 就可以从这系列 URL 指定的资源中加载指定类,这里的 URL 可以以 file: 为前缀,表明从本地文件系统加载;可以以 http: 为前缀,表明从互联网通过 HTTP 访问来加载;也可以以 ftp: 为前缀,表明从互联网通过 FTP访问来加载......功能非常强大。
以上就是浅谈JAVA 类加载器的详细内容,更多关于JAVA 类加载器的资料请关注我们其它相关文章!
时间:2020-06-22
我们知道,我们写的java代码保存的格式是 .java, java文件被编译后会转换为字节码,字节码可以在任何平台通过java虚拟机来运行,这也是java能够跨平台的原因. 那JVM是如何来让我们写的java文件运行的呢? 这个问题通常的问法好像是:类是如何被加载的. 记得第一次遇见这个问题的时候,同学给我的回答是: 1.虚拟机会加载JDK里类的核心包 2.虚拟机会加载JDK里类的扩展包 3.虚拟机会加载JDK里类的系统包 4.虚拟机再会加载我们写好的java类. 初学的时候,大家都这么说,好像
如果要使用自定义类加载器加载class文件,就需要继承java.lang.ClassLoader类. ClassLoader有几个重要的方法: protectedClassLoader(ClassLoaderparent):使用指定的.用于委托操作的父类加载器创建新的类加载器. protectedfinalClass<?>defineClass(Stringname,byte[]b,intoff,intlen):将一个byte数组转换为Class类的实例. protectedClass<
这篇文章主要介绍了Java类加载器ClassLoader用法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 正文 当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载.连接.初始化3个步骤来对该类进行初始化.如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化. 一.类加载过程 1.加载 加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说
本文实例讲述了Java实现的自定义类加载器.分享给大家供大家参考,具体如下: 一 点睛 1 ClassLoader类有如下两个关键方法: loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象. findClass(String name):根据二进制名称来查找类. 如果需要实现自定义的ClassLoader,可以通过重写以上两
热部署: 热部署就是在不重启应用的情况下,当类的定义即字节码文件修改后,能够替换该Class创建的对象.一般情况下,类的加载都是由系统自带的类加载器完成,且对于同一个全限定名的java类,只能被加载一次,而且无法被卸载.可以使用自定义的 ClassLoader 替换系统的加载器,创建一个新的 ClassLoader,再用它加载 Class,得到的 Class 对象就是新的(因为不是同一个类加载器),再用该 Class 对象创建一个实例,从而实现动态更新.如:修改 JSP 文件即生效,就是利用自定
疑惑 以前在看源码的时候,总是会遇到框架里的代码使用Thread.currentThread.getContextClassLoader()获取当前线程的Context类加载器,通过这个Context类加载器去加载类. 我们平时在程序中写代码的时候,遇到要动态加载类的时候,一般使用Class.forName()的方式加载我们需要的类.比如最常见的,当我们进行JDBC编程的时候,我们通过Class.forName()去加载JDBC的驱动. try { return Class.forName("o
本文研究的主要是Java语言中的自定义类加载器实例解析的相关内容,具体如下. 自己写的类加载器 需要注意的是:如果想要对这个实例进行测试的话,首先需要在c盘建立一个c://myjava的目录.然后将相应的java文件放在这个目录中.并将产生的.clas文件放在c://myjava/com/lg.test目录下,否则是找不到的.这是要注意的.. class FileClassLoader : package com.lg.test; import java.io.ByteArrayOutputSt
用类加载器的5中形式读取.properties文件(这个.properties文件一般放在src的下面) 用类加载器进行读取:这里采取先向大家讲读取类加载器的几种方法:然后写一个例子把几种方法融进去,让大家直观感受.最后分析原理.(主要是结合所牵涉的方法的源代码的角度进行分析) 这里先介绍用类加载器读取的几种方法: 1.任意类名.class.getResourceAsStream("/文件所在的位置");[文件所在的位置从包名开始写] 2.和.properties文件在同一个目录下的类
本文实例讲述了Java类加载器和类加载机制.分享给大家供大家参考,具体如下: 一 点睛 1 类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象. 2 当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构: Bootstrap ClassLoader:根类加载器. Extension ClassLoader:扩展类加载器. System ClassLoader:系统类加载器. 3 JVM的类加载机制主要有如下三种
RTTI,即Run-Time Type Identification,运行时类型识别.运行时类型识别是Java中非常有用的机制,在Java运行时,RTTI维护类的相关信息.RTTI能在运行时就能够自动识别每个编译时已知的类型. 很多时候需要进行向上转型,比如Base类派生出Derived类,但是现有的方法只需要将Base对象作为参数,实际传入的则是其派生类的引用.那么RTTI就在此时起到了作用,比如通过RTTI能识别出Derive类是Base的派生类,这样就能够向上转型为Derived.类似的,
Android 图片的三级缓存机制实例分析 当我们获取图片的时候,如果不加以协调好图片的缓存,就会造成大流量,费流量应用,用户体验不好,影响后期发展.为此,我特地分享Android图片的三级缓存机制之从网络中获取图片,来优化应用,具体分三步进行: (1)从缓存中获取图片 (2)从本地的缓存目录中获取图片,并且获取到之后,放到缓存中 (3)从网络去下载图片,下载完成之后,保存到本地和放到缓存中 很好的协调这三层图片缓存就可以大幅度提升应用的性能和用户体验. 快速实现三级缓存的工具类ImageCac
Java List 用法详解及实例分析 Java中可变数组的原理就是不断的创建新的数组,将原数组加到新的数组中,下文对Java List用法做了详解. List:元素是有序的(怎么存的就怎么取出来,顺序不会乱),元素可以重复(角标1上有个3,角标2上也可以有个3)因为该集合体系有索引 ArrayList:底层的数据结构使用的是数组结构(数组长度是可变的百分之五十延长)(特点是查询很快,但增删较慢)线程不同步 LinkedList:底层的数据结构是链表结构(特点是查询较慢,增删较快) Vector
本文实例讲述了Java设计模式之静态代理模式.分享给大家供大家参考,具体如下: 代理模式,可以通过代理可以在原来的基础上附加一些其他的操作.静态代理模式相对比较简单无需再程序运行时动态的进行代理. 静态代理模式的角色: ① 抽象角色:真实对象和代理对象的共同接口.其中声明真实对象和代理对象需要做的事. ② 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用. ③ 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作. 下面提
本文实例讲述了Yii2框架类自动加载机制.分享给大家供大家参考,具体如下: 在yii中,程序中需要使用到的类无需事先加载其类文件,在使用的时候才自动定位类文件位置并加载之,这么高效的运行方式得益于yii的类自动加载机制. Yii的类自动加载实际上使用的是PHP的类自动加载,所以先来看看PHP的类自动加载.在PHP中,当程序中使用的类未加载时,在报错之前会先调用魔术方法__autoload(),所以我们可以重写__autoload()方法,定义当一个类找不到的时候怎么去根据类名称找到对应的文件并加
本文实例讲述了Java设计模式之动态代理模式.分享给大家供大家参考,具体如下: 前面介绍了静态代理模式,动态代理比静态代理模式更加强大.它能在程序运行时动态的生成代理对象.所谓动态代理类是在运行时生成的class,在生成它时,你必须提供一组interface给它,则动态代理类就宣称它实现了这些interface.当然,动态代理类就充当一个代理,你不要企图它会帮你干实质性的工作,在生成它的实例时你必须提供一个handler,由它接管实际的工作. 动态代理的角色和静态代理的角色一样: ① 抽象角色:
本文实例分析了Java中List与数组相互转换的方法.分享给大家供大家参考.具体如下: 今天写代码遇到一个奇怪的问题,具体代码不贴出了,写一个简化的版本.如下: ArrayList<String> list=new ArrayList<String>(); String strings[]=(String [])list.toArray(); 这样写代码个人觉得应该没什么问题,编译也没有问题.可是具体运行的时候报异常,如下:Exception in thread "mai
本文实例讲述了Android开发之Parcel机制.分享给大家供大家参考.具体分析如下: 在java中,有序列化机制.但是在安卓设备上,由于内存有限,所以设计了新的序列化机制. Container for a message (data and object references) that can be sent through an IBinder. A Parcel can contain both flattened data that will be unflattened on t
本文实例分析了Python装饰器的执行过程.分享给大家供大家参考,具体如下: 今天看到一句话:装饰器其实就是对闭包的使用,仔细想想,其实就是这回事,今天又看了下闭包,基本上算是弄明白了闭包的执行过程了.其实加上几句话以后就可以很容易的发现,思路给读者,最好自己总结一下,有助于理解.通过代码来说吧. 第一种,装饰器本身不传参数,相对来说过程相对简单的 #!/usr/bin/python #coding: utf-8 # 装饰器其实就是对闭包的使用 def dec(fun): print("call