前言

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

自定义类加载器

例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. 聊聊 Jmeter 如何并发执行 Python 脚本

    1. 前言 大家好,我是安果! 最近有小伙伴后台给我留言,说自己用 Django 写了一个大文件上传的 Api 接口,现在想本地检验一下接口并发的稳定性,问我有没有好的方案 本篇文章以文件上传为例,聊 ...

  2. MySQL——日志管理

    一.MySQL日志类型 1.错误:--log--error ---------------------*** host_name.err 2.常规: --general_log host_name.l ...

  3. Spring Cloud Apollo 实践

    接上一篇Windows下安装Apollo的常见问题,安装完毕后试着看怎么来使用一下. 首先到管理页面创建一个新的应用: 创建成功后会自动跳转到应用的维护界面,如下图所示: 新增一个配置信息来进行后续的 ...

  4. RabbitMQ-如何保证消息在99.99%的情况下不丢失

    1. 简介 MQ虽然帮我们解决了很多问题,但是也带来了很多问题,其中最麻烦的就是,如何保证消息的可靠性传输. 我们在聊如何保证消息的可靠性传输之前,先考虑下哪些情况下会出现消息丢失的情况. 首先,上图 ...

  5. try catch处理流的异常

    1.try catch处理异常 try{} catch(Exception e){} finally{ 必然执行的代码,一般是释放资源 } 2.流使用try catch处理异常 其中,变量作用域只在当 ...

  6. 数据导出生成Excel附件使用POI的HSSFWorkbook对象

    比较常用的实现Java导入.导出Excel的技术有两种Jakarta POI和Java Excel.Jakarta POI 是一套用于访问微软格式文档的Java API.Jakarta POI有很多组 ...

  7. 再谈java线程

    线程状态 描述 当线程被创建并启动之后,它既不是已启动就进入到了执行状态,也不是一直处于执行状态.在线程的声明周期中有六中状态. java api中java.lang.Thread.State这个枚举 ...

  8. bash-completion linux命令补全

    1.有时候用docker run 或者kubectl 想tab补全的时候用不了 这个时候可以安装一个神奇的包bash-completion yum install bash-completion 2. ...

  9. PHP中命名空间是怎样的存在?(二)

    今天带来的依然是命名空间相关的内容,本身命名空间就是PHP中非常重要的一个特性.所以关于它的各种操作和使用还是非常复杂的,光使用方式就有很多种,我们一个一个的来看. 子命名空间 命名空间本身就像目录一 ...

  10. 基于AM335X,如何搭建优良的Linux开发环境(下)

    接着上一篇文章的Linux开发环境搭建,文章中详细讲解了 VMware14.1.1虚拟机安装.基于虚拟机安装Ubuntu14.04.3操作系统.安装Ubuntu14.04.3操作系统.安装虚拟机工具. ...