前言

学习类加载器就一定要自己实现一个类加载器,今天就从一个简单的自定义类加载器说起。

自定义类加载器

例1

一个简单的类加载器,从一个给定的二进制名字读取一个字节码文件的内容,然后生成对应的class对象。

package com.jamie.jvmstudy;

import java.io.*;

public class CustomizedClassLoader extends ClassLoader {

    private String classLoaderName;

    private String fileExtension = ".class";

    public CustomizedClassLoader(String classLoaderName) {
super(); //如果调用默认构造器,代表默认的父类加载器是系统类加载器SystemClassLoader
this.classLoaderName = classLoaderName;
} public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
super(parent); //如果指定父类加载器,那么该构造器执行完之后,这个类加载器就有指定的parent了。(默认是系统类加载器)
this.classLoaderName = classLoaderName;
} @Override
public Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = this.loadClassData(className); return this.defineClass(className, data, 0, data.length);
} private byte[] loadClassData(String className) {
byte[] data = null;
try(InputStream is = new FileInputStream(new File(className + this.fileExtension));
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch;
while(-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
} public static void main(String[] args) throws Exception {
CustomizedClassLoader customizedClassLoader = new CustomizedClassLoader("jamie loader");
test(customizedClassLoader);
} private static void test(ClassLoader classLoader) throws Exception {
Class<?> myClass = classLoader.loadClass("com.jamie.jvmstudy.TestClassLoader");
Object o = myClass.newInstance();
System.out.println(String.format("classLoader in this method is [%s]", classLoader));
System.out.println(String.format("object [%s] has been created by [%s].", o, myClass.getClassLoader()));
System.out.println(String.format("class loader of CustomizedClassLoader is [%s]", classLoader.getClass().getClassLoader()));
}
}

运行结果如下:

classLoader in this method is [com.jamie.jvmstudy.CustomizedClassLoader@4b67cf4d]
object [com.jamie.jvmstudy.TestClassLoader@7ea987ac] has been created by [sun.misc.Launcher$AppClassLoader@14dad5dc].
class loader of CustomizedClassLoader is [sun.misc.Launcher$AppClassLoader@14dad5dc]

这里重点说明一下:示例中的"com.jamie.jvmstudy.TestClassLoader"Class是被系统类加载器加载的,而不是我们自定义的加载器。

原因:

  1. 自定义的加载器CustomizedClassLoader的父类构造器是系统类加载器。因为我们加载类调用的方法是:classLoader.loadClass("com.jamie.jvmstudy.TestClassLoader")
  2. 默认的loadClass()方法实现就是双亲委派的源码实现,因为系统类加载器会在当前的classpath(类路径)下查找是否存在匹配的"binary name",如果存在,则系统类加载器加载成功。
  3. 所以只要类路径下存在匹配的二进制名字的字节码,就会被系统类加载器成功加载。

例2

基于例1的基础上,自定义加载器获取字节码文件的内容改为从一个指定的路径中读取。并且传入一个非classpath的路径,去加载某个字节码。

因为自定义的类加载器默认的父类加载器是系统类加载器,运行下例的时候,需要在编译之后把类路径下的com.jamie.jvmstudy.TestClassLoader字节码文件删除(防止[双亲委托机制]使[系统类加载器AppClassLoader]把指定类加载进虚拟机),然后运行本例。

package com.jamie.jvmstudy;

import java.io.*;

public class CustomizedClassLoader extends ClassLoader {

    private String classLoaderName;

    private String path;

    private String fileExtension = ".class";

    public CustomizedClassLoader(String classLoaderName) {
super(); //如果调用默认构造器,代表默认的父类加载器是系统类加载器SystemClassLoader
this.classLoaderName = classLoaderName;
} public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
super(parent);
this.classLoaderName = classLoaderName;
} @Override
public Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("Self findClass() invoked");
byte[] data = this.loadClassData(className);
return this.defineClass(className, data, 0, data.length);
} private byte[] loadClassData(String className) {
byte[] data = null;
className = className.replace(".", "/");
try(InputStream is = new FileInputStream(new File(path + className + this.fileExtension));
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch;
while(-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
} public static void main(String[] args) throws Exception {
CustomizedClassLoader loader1 = new CustomizedClassLoader("jamie loader1");
loader1.setPath("D:/temp/");
Class<?> myClass1 = loader1.loadClass("com.jamie.jvmstudy.TestClassLoader");
System.out.println(String.format("Hashcode of myClass1 is [%s].", myClass1.hashCode()));
System.out.println(String.format("myClass1 is [%s]", myClass1));
System.out.println(); CustomizedClassLoader loader2 = new CustomizedClassLoader("jamie loader2");
loader2.setPath("D:/temp/");
Class<?> myClass2 = loader2.loadClass("com.jamie.jvmstudy.TestClassLoader");
System.out.println(String.format("Hashcode of myClass2 is [%s].", myClass2.hashCode()));
System.out.println(String.format("myClass2 is [%s]", myClass2)); System.out.println();
System.out.println("myClass1 == myClass2 ? " + (myClass1 == myClass2));
} public void setPath(String path) {
this.path = path;
}
}

运行结果如下:

Self findClass() invoked
Hashcode of myClass1 is [1956725890].
myClass1 is [class com.jamie.jvmstudy.TestClassLoader] Self findClass() invoked
Hashcode of myClass2 is [21685669].
myClass2 is [class com.jamie.jvmstudy.TestClassLoader] myClass1 == myClass2 ? false

结论:由运行结果可以看出,此时的虚拟机中出现了两个不一样的TestClassLoader.class对象。

这就引入了类加载器的命名空间的问题。

我们之前理解的“Class对象只存在一份”是基于同一个类加载器的命名空间来说的。

类加载器的命名空间

  • 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

后续写了一篇文章类加载器之命名空间详解,里面有对【命名空间】更加详细的分析与示例。感兴趣请跳转。

类的卸载

  • 当一个类被加载、连接和初始化之后,它的生命周期就开始了。当代表该类的Class对象不再被引用,既不可达时,Class对象就会结束生命周期,该类在方法区内的数据也会被卸载,从而结束该类的生命周期。
  • 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
  • 被Java虚拟机自带的ClassLoader加载的类,是不会被卸载的。因为JVM本身会始终引用这些ClassLoader,而这些ClassLoader始终会引用它加载的所有类的Class对象。所以它们永远可达。
  • 只有自定义的类加载器加载的类,才有可能被卸载。

类卸载的证明:

基于上述例2,改造一下main方法,并在执行时,添加JVM运行参数:-XX:+TraceClassUnloading

    public static void main(String[] args) throws Exception {
CustomizedClassLoader loader1 = new CustomizedClassLoader("jamie loader1");
loader1.setPath("D:/temp/");
Class<?> myClass1 = loader1.loadClass("com.jamie.jvmstudy.TestClassLoader");
System.out.println(String.format("Hashcode of myClass1 is [%s].", myClass1.hashCode()));
System.out.println(String.format("myClass1 is [%s]", myClass1));
System.out.println(); loader1 = null;
myClass1 = null;
System.gc(); CustomizedClassLoader loader2 = new CustomizedClassLoader("jamie loader2");
loader2.setPath("D:/temp/");
Class<?> myClass2 = loader2.loadClass("com.jamie.jvmstudy.TestClassLoader");
System.out.println(String.format("Hashcode of myClass2 is [%s].", myClass2.hashCode()));
System.out.println(String.format("myClass2 is [%s]", myClass2)); System.out.println();
System.out.println("myClass1 == myClass2 ? " + (myClass1 == myClass2));
Thread.sleep(50000); //为了查看类的卸载情况增加延时
}

运行结果:可以看到TestClassLoader类被卸载一次。

=====Self findClass() invoked=====
Hashcode of myClass1 is [1956725890].
myClass1 is [class com.jamie.jvmstudy.TestClassLoader] [Unloading class com.jamie.jvmstudy.TestClassLoader 0x00000007c0061028]
=====Self findClass() invoked=====
Hashcode of myClass2 is [21685669].
myClass2 is [class com.jamie.jvmstudy.TestClassLoader] myClass1 == myClass2 ? false

增加main方法睡眠时间,使用Java VisualVM可以看到下图:

【Java虚拟机8】自定义类加载器、类加载器命名空间、类的卸载的更多相关文章

  1. 【Java虚拟机11】线程上下文类加载器

    前言 目前学习到的类加载的知识,都是基于[双亲委托机制]的.那么JDK难道就没有提供一种打破双亲委托机制的类加载机制吗? 答案是否定的. JDK为我们提供了一种打破双亲委托模型的机制:线程上下文类加载 ...

  2. (转)《深入理解java虚拟机》学习笔记6——类加载机制

    Java虚拟机类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程. 在加载阶段,java虚拟机需要完成以下 ...

  3. 《深入理解Java虚拟机》学习笔记之类加载

    之前在学习ASM时做了一篇笔记<Java字节码操纵框架ASM小试>,笔记里对类文件结构做了简介,这里我们来回顾一下. Class类文件结构 在Java发展之初设计者们发布规范文档时就刻意把 ...

  4. 《深入java虚拟机》读书笔记之垃圾收集器与内存分配策略

    前言 该读书笔记用于记录在学习<深入理解Java虚拟机--JVM高级特性与最佳实践>一书中的一些重要知识点,对其中的部分内容进行归纳,或者是对其中不明白的地方做一些注释.主要是方便之后进行 ...

  5. 《深入理解Java虚拟机》-----第9章 类加载及执行子系统的案例与实战

    概述 在Class文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多, Class文件以何种格式存储,类型何时加载.如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户 ...

  6. 《深入理解Java虚拟机》-----第3章 垃圾收集器与内存分配策略

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来. 3.1 概述 说起垃圾收集(Garbage Collection,GC),大部分人都把这 ...

  7. 《深入理解Java虚拟机》(三)垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 详解 3.1 概述 本文参考的是周志明的 <深入理解Java虚拟机>第三章 ,为了整理思路,简单记录一下,方便后期查阅. 3.2 对象已死吗 在垃圾收集器进行回收 ...

  8. 《深入理解 Java 虚拟机》读书笔记:垃圾收集器与内存分配策略

    正文 垃圾收集器关注的是 Java 堆和方法区,因为这部分内存的分配和回收是动态的.只有在程序处于运行期间时才能知道会创建哪些对象,也才能知道需要多少内存. 虚拟机栈和本地方法栈则不需要过多考虑回收的 ...

  9. 从Java虚拟机的内存区域、垃圾收集器及内存分配原则谈Java的内存回收机制

    一.引言: 在Java中我们只需要轻轻地new一下,就可以为实例化一个类,并分配对应的内存空间,而后似乎我们也可以不用去管它,Java自带垃圾回收器,到了对象死亡的时候垃圾回收器就会将死亡对象的内存回 ...

  10. 深入理解Java虚拟机读书笔记2----垃圾收集器与内存分配策略

    二 垃圾收集器与内存分配策略 1 JVM中哪些内存需要回收?     JVM垃圾回收主要关注的是Java堆和方法区这两个区域:而程序计数器.虚拟机栈.本地方法栈这3个区域随线程而生,随线程而灭,随着方 ...

随机推荐

  1. 【曹工杂谈】Maven源码调试工程搭建

    Maven源码调试工程搭建 思路 我们前面的文章<[曹工杂谈]Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗>分析了Maven大体的执行阶段,主要包括三个阶段: 启动类阶段,负责 ...

  2. css文本溢出省略号大总结,如你所愿

    一行: white-space: nowrap; text-overflow: ellipsis; overflow: hidden; word-break: break-all; 两行: width ...

  3. python3 爬虫五大模块之五:信息采集器

    Python的爬虫框架主要可以分为以下五个部分: 爬虫调度器:用于各个模块之间的通信,可以理解为爬虫的入口与核心(main函数),爬虫的执行策略在此模块进行定义: URL管理器:负责URL的管理,包括 ...

  4. Node.js躬行记(9)——微前端实践

    后台管理系统使用的是umi框架,随着公司业务的发展,目前已经变成了一个巨石应用,越来越难维护,有必要对其进行拆分了. 计划是从市面上挑选一个成熟的微前端框架,首先选择的是 icestark,虽然文档中 ...

  5. 通过HttpURLConnection下载图片到本地--批量下载

    一.背景说明 这篇文章讲述的是批量下载附件,在上一篇文章中,介绍了下载单个附件(上一篇文章). 二.实现思路 主要的实现思路:创建文件夹->文件夹中创建需要下载的文件->压缩文件夹-> ...

  6. 【第六篇】- Maven 仓库之Spring Cloud直播商城 b2b2c电子商务技术总结

    Maven 仓库 在 Maven 的术语中,仓库是一个位置(place). Maven 仓库是项目中依赖的第三方库,这个库所在的位置叫做仓库. 在 Maven 中,任何一个依赖.插件或者项目构建的输出 ...

  7. Spring Cloud Eureka 之常用配置解析

    [原创内容,转载.引用请注明出处] 1. 配置项解析 1.1 通用配置 # 应用名称,将会显示在Eureka界面的应用名称列 spring.application.name=config-servic ...

  8. Percolator模型及其在TiKV中的实现

    一.背景 Percolator是Google在2010年发表的论文<Large-scale Incremental Processing Using Distributed Transactio ...

  9. html2canvas实现截取指定区域或iframe的区域

    官网文档: http://html2canvas.hertzen.com/ 使用的是 jquery 3.2.1   html2canvas 1.0.0-rc.7 截取根据id的指定区域: var ca ...

  10. Docker系列(5)- 常用命令(1) | 帮助命令

    帮助命令 [root@localhost ~]# docker version #显示docker的版本信息 [root@localhost ~]# docker info #显示docker的系统信 ...