JVM体系结构之二:类加载器之2:JVM 自定义的类加载器的实现和使用
一、回顾一下jdk自带的类加载器:
1.java虚拟机自带的加载器
根类加载器(Bootstrap,c++实现)
扩展类加载器(Extension,java实现)
应用类加载器(App,java实现)
2.用户自定义的类加载器
java.lang.ClassLoader的子类
用户可以定制类的加载方式
java的类加载机制默认情况下是采用委托模型:当加载某个类时JVM会首先尝试用当前类加载器的父类加载器加载该类,若父类加载器加载不到再由当前类加载器来加载,因此这种模型又叫做“父优先”模型。
但是在实际项目中我们可能会要求先从当前类加载加载再从父类加载器加载,如项目中的某类的版本可能和container中的不一致的时候,若还从container加载就会报jar包冲突的异常,实际上jar包冲突的问题在实际开发过程中是经常会遇到的。
解决方案是通过扩展自定义的ClassLoader,重写loadClass方法,先从当前类加载器加载再从父类加载器加载。
二、用户自定义的类加载器:
要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定类的名字,返回对应的Class对象的引用。
protected Class findClass(String s)
throws ClassNotFoundException
{
throw new ClassNotFoundException(s);
}
示例:创建3个自定义类加载器,分别对应的父加载器如下图:

自定义类加载器:
package com.dxz.classloader; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream; public class MyClassLoader extends ClassLoader { // 类加载器名称
private String name;
// 加载类的路径
private String path = "E:/";
private final String fileType = ".class"; public MyClassLoader(String name) {
// 让系统类加载器成为该类加载器的父加载器
super();
this.name = name;
} public MyClassLoader(ClassLoader parent, String name) {
// 显示指定该类加载器的父加载器
super(parent);
this.name = name;
} public String getPath() {
return path;
} public void setPath(String path) {
this.path = path;
} @Override
public String toString() {
return this.name;
} /**
* 获取.class文件的字节数组
*
* @param name
* @return
*/
private byte[] loaderClassData(String className) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
className = className.replace(".", "/");
try {
is = new FileInputStream(new File(path + className + fileType));
int c = 0;
while (-1 != (c = is.read())) {
baos.write(c);
}
data = baos.toByteArray(); } catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
} /**
* 获取Class对象
*/
@Override
public Class<?> findClass(String name) {
byte[] data = loaderClassData(name);
return this.defineClass(name, data, 0, data.length);
} public static void main(String[] args)
throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// loader1的父加载器为系统类加载器
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("E:/duan/lib1/");
// loader2的父加载器为loader1
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("E:/duan/lib2/");
// loader3的父加载器为根类加载器
MyClassLoader loader3 = new MyClassLoader(null, "loader3");
loader3.setPath("E:/duan/lib3/"); Class clazz = loader2.loadClass("com.dxz.classloader.Sample");
Object object = clazz.newInstance();
//object.main(null);
}
}
package com.dxz.classloader;
public class Hello {
public Hello() {
System.out.println("hello");
}
}
被加载类-情况1:
package com.dxz.classloader;
public class Sample {
private String name = "hello world";
public Sample() {
System.out.println("one");
new Hello();
}
public static void main(String[] args) {
System.out.println(new Sample().name);
}
}
并将生成的Sample.class文件拷贝到指定的目录下:

被加载类-情况2:
package com.dxz.classloader;
public class Sample {
private String name = "hello world";
public Sample() {
System.out.println("two");
new Hello();
}
public static void main(String[] args) {
System.out.println(new Sample().name);
}
}
将新生成的Sample.class文件拷贝lib2下

被加载类-情况3:
package com.dxz.classloader;
public class Sample {
private String name = "hello world";
public Sample() {
System.out.println("three");
new Hello();
}
public static void main(String[] args) {
System.out.println(new Sample().name);
}
}
将新生成的Sample.class文件拷贝lib3下

测试场景1:
// loader1的父加载器为系统类加载器
MyClassLoader loader1 = null;//new MyClassLoader("loader1");
//loader1.setPath("E:/duan/lib1/");
// loader2的父加载器为loader1
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("E:/duan/lib2/");
如果loader2的父加载器为空,则loader2加载器会去加载E:/duan/lib2/下的Sample.class文件,结果如下:

测试场景2:
// loader1的父加载器为系统类加载器
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("E:/duan/lib1/");
// loader2的父加载器为loader1
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("E:/duan/lib2/");
结果:

结果说明:
当执行loader2.loaderClass("com.dxz.classloader.Sample")时,先由它上层的所有父加载器尝试加载Sample类。如果父加载loader1不为空,则loader1从D:/lib1/目录下成功的加载了Sample类,因此laoder1是Sample类的定义类加载器,loader1和loader2是Sample类的初始类加载器。
当执行loader3.loadClass("com.dxz.classloader.Sample")时,先由它上层的所有父加载器尝试加载Sample类。loader3的父加载器为根类加载器,它无法加载Sample类,接着loader3从D:/lib3/目录下成功地加载了Sample类,因此loader3是Sample类的定义类加载器即初始类加载器。

在Sample类中主动new Hello类,当执行Sample类的构造方法中的new Hello()语句时,Java虚拟机需要先加载Hello类,Java虚拟机会按Sample类的定义类加载器去加载Hello类,加载过程也同样采用父亲委托机制。
三、总结
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:
1、这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
3、这类 Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
双亲委派机制能很好地解决类加载的统一性问题。对一个 Class 对象来说,如果类加载器不同,即便是同一个字节码文件,生成的 Class 对象也是不等的。也就是说,类加载器相当于 Class 对象的一个命名空间。双亲委派机制则保证了基类都由相同的类加载器加载,这样就避免了同一个字节码文件被多次加载生成不同的 Class 对象的问题。但双亲委派机制仅仅是Java 规范所推荐的一种实现方式,它并不是强制性的要求。近年来,很多热部署的技术都已不遵循这一规则,如 OSGi 技术就采用了一种网状的结构,而非双亲委派机制。
JVM体系结构之二:类加载器之2:JVM 自定义的类加载器的实现和使用的更多相关文章
- JVM体系结构之二:类加载器
一.概述 定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型.类加载和连接的过程都是在运行期间完成的. 二. 类的加载 ...
- JVM学习十二 - (复习)JVM内存结构
JVM 内存结构 Java 虚拟机的内存空间分为 5 个部分: 程序计数器 Java 虚拟机栈 本地方法栈 堆 方法区 JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代.元 ...
- JVM 体系结构与工作方式
.katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...
- JVM学习二:JVM之类加载器之加载分析
前面一遍,我们对类的加载有了一个整体的认识,而这一节我们细节分析一下类加载器的第一步,即:加载. 一.概念 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区 ...
- java之jvm学习笔记二(类装载器的体系结构)
java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种, 第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新 ...
- JVM学习六:JVM之类加载器之双亲委派机制
前面我们知道类加载有系统自带的3种加载器,也有自定义的加载器,那么这些加载器之间的关系是什么,已经在加载类的时候,谁去加载呢?这节,我们将进行讲解. 一.双亲委派机制 JVM的ClassLoader采 ...
- JVM系列(二)之类加载
什么是类的加载 类加载是指将源代码编译后的.class加载到内存中初始化待程序使用的过程,类加载的最终结果就是将.class字节码加载到JVM中生成一个java.lang.Class对象,提供给程序使 ...
- JVM 体系结构
JVM 是一种抽象的计算机,基于堆栈架构,它有自己的指令集和内存管理,是 Java 跨平台的依据,JVM解释执行字节码,或将字节码编译成本地代码执行.Java 虚拟机体系结构如下: Class Fil ...
- JVM体系结构-----深入理解内存结构
一.概述 内存在计算机中占据着至关重要的地位,任何运行时的程序或者数据都需要依靠内存作为存储介质,否则程序将无法正常运行.与C和C++相比,使用Java语言编写的程序并不需要显示的为每一个对象编写对应 ...
随机推荐
- 洛谷P2402 奶牛隐藏
洛谷P2402 奶牛隐藏 题目背景 这本是一个非常简单的问题,然而奶牛们由于下雨已经非常混乱,无法完成这一计算,于是这个任务就交给了你.(奶牛混乱的原因看题目描述) 题目描述 在一个农场里有n块田地. ...
- LookAndFeelInfo
在读zap源码的时候碰到:UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());以下来自百度知道UIManager.s ...
- dygraphs for R
dygraphs一个功能非常强大的处理时间序列的画图包!画出的图在html中打开,鼠标点处,即可得到数据信息.详情见 http://rstudio.github.io/dygraphs/index.h ...
- HTML5离线存储和本地缓存
一.离线存储 有一个web应用有三个文件index.html,a.js,b.css,现在需要把js和css文件缓存起来 1.在index.html里加上<html manifest=" ...
- Ionic background地址写法问题
1.背景图片 background:url(‘/img/text.jpg') 这种写法在手机上不好使 ’../img/text.jpg' 这种在手机上好使
- linux shell 基础 使用日志与心得
linux shell 基础 使用日志与心得 1.#!/bin/bash 第一行就出现#!/bin/bash是指此脚本使用/bin/bash来解释执行.其中,#!是一个特殊的表示符,其后,跟着解释此脚 ...
- DEV开发之控件XtraTabbedMdiManager
使用的时候要先设置窗体的IsMdiZContainer属性为True然后再窗体上新增XtraTabbedMdiManager控件 设置属性:HeaderLocation为bottomClosePage ...
- day3--集合、文件操作、字符编码与转换、函数(递归,lambda,filter,map)、字典排序
list1 = set([1, 2, 3, 4, 5, 6, 5, 5, 5])list2 = set([11, 2, 36, 'a', 5, 6, 5, 5, 5])list3 = set([1, ...
- Docker 数据管理-三种数据mount方式
可以在Container可写层存储数据,但是有三个缺点: 当Container销毁时,数据不能持久保存. Container的可写层和Container所在的主机紧耦合,不容易将数据移动到其他地方. ...
- curl 监控web
[root@rhel6 ~]# curl -I -s -w "%{http_code}\n" -o /dev/null http://127.0.0.1 [root@rhel6 ~ ...