前言

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

自定义类加载器

例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. Tensorflow之TFRecord的原理和使用心得

    本文首发于微信公众号「对白的算法屋」 大家好,我是对白. 目前,越来越多的互联网公司内部都有自己的一套框架去训练模型,而模型训练时需要的数据则都保存在分布式文件系统(HDFS)上.Hive作为构建在H ...

  2. springboot通过AOP和自定义注解实现权限校验

    自定义注解 PermissionCheck: package com.mgdd.sys.annotation; import java.lang.annotation.*; /** * @author ...

  3. WebService学习总结(三)--调用第三方提供的webService服务

    互联网上面有很多的免费webService服务,我们可以调用这些免费的WebService服务,将一些其他网站的内容信息集成到我们的Web应用中显示,下面就以获取电子邮箱验证和查询火车时刻表和天气预报 ...

  4. AFL++ Fuzz一个libexif例子

    CVE-2009-3895 首先在NVD找到漏洞描述如下: 大致意思是说:libexif 0.6.18 中的 libexif/exif-entry.c 中的 exif_entry_fix 函数中基于堆 ...

  5. SQL语句之基本使用

    1.sql语法 一些重要的SQL命令: SELECT - 从数据库中提取数据 UPDATE - 更新数据库中的数据 DELETE - 从数据库中删除数据 INSERT INTO - 向数据库中插入新数 ...

  6. java多线程 synchronized 与lock锁 实现线程安全

    如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. 通过卖火车票的例子 火车站要卖票,我们模 ...

  7. Operator 示例:使用 Redis 部署 PHP 留言板应用程序

    「Kubernetes 官方示例:使用 Redis 部署 PHP 留言板应用程序」Operator 化. 源码仓库:https://github.com/jxlwqq/guestbook-operat ...

  8. CodeForce-798C Mike and gcd problem(贪心)

    Mike has a sequence A = [a1, a2, ..., an] of length n. He considers the sequence B = [b1, b2, ..., b ...

  9. 【C++基础教程】第四课

    上次的课后练习 第1题输出: 第二题输出:1 第三题: #include<iostream> #include<cmath> using namespace std; int ...

  10. (超简单)VScode配置C/C++环境图文教程(Windows系统下)

    (超简单)VScode配置C/C++环境图文教程(Windows系统下) 本文参考文章 Visual Studio Code (vscode) 配置 C / C++ 环境 下载VScode.下载Min ...