一, 类加载器深入剖析
1,Java虚拟机与程序的生命周期
在如下几种情况下,Java虚拟机将结束生命周期:
–执行了System.exit()方法
–程序正常执行结束
–程序在执行过程中遇到了异常或错误而异常终止
–由于操作系统出现错误而导致Java虚拟机进程终止
2,类的加载,链接,初始化
概念:
•加载:查找并加载类的二进制数据(java编译后的.class文件)
•连接
–验证:确保被加载的类的正确性
–准备:为类的静态变量分配内存,并将其初始化为默认值
–解析:把类中的符号引用转换为直接引用
•初始化:为类的静态变量赋予正确的初始值(=号后面的值)
图示:
初始化的条件:
•Java程序对类的使用方式可分为两种
–主动使用
–被动使用
•所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
•主动使用(六种)
–创建类的实例
–访问某个类或接口的静态变量,或者对该静态变量赋值
–调用类的静态方法
–反射(如Class.forName(“com.shengsiyuan.Test”))
–初始化一个类的子类
–Java虚拟机启动时被标明为启动类的类(Java Test),就是用java命令执行的那个带有main方法入口的类。
被动使用
除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
类的加载:
1,类加载做的事情:
•类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个该类的java.lang.Class对象,用来封装类在方法区内的数据结构。
JVM运行时数据区的解释:
运行时数据区可以理解为java虚拟机内存。
JVM就是一个特殊的进程, 我们执行的java程序, 都运行在一个JVM进程中, 这个进程的作用就是加载class文件, 并且执行class文件中的代码。 当然, 从一个class文件的加载, 到准备好可执行之前, 还有一段很长的路要走。 既然虚拟机作为一个虚拟的计算机, 来执行我们的程序:
那么在执行的过程中,必然要有地方存放我们的代码(class文件);
在执行的过程中, 总会创建很多对象, 必须有地方存放这些对象;
在执行的过程中, 还需要保存一些执行的状态, 比如, 将要执行哪个方法, 当前方法执行完成之后, 要返回到哪个方法等信息;
所以, 必须有一个地方来保持执行的状态。 上面的描述中, “地方”指的当然就是内存区域, 程序运行起来之后, 就是一个动态的过程, 必须合理的划分内存区域, 来存放各种数据(内存,用于存放程序运行时数据)。
事实上,JVM在执行Java代码时都会把内存分为几个部分,即数据区来使用,这些区域都拥有自己的用途,并随着JVM进程的启动或者用户线程的启动和结束建立和销毁。接下去,通过下面的这幅图,我们一个一个细数一下JVM运行时的数据区结构。
线程私有的数据区
程序计数器
· 作用
记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
· 意义
JVM的多线程是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器。
· 存储内容
当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。
当线程中执行的是一个本地方法时,程序计数器中的值为空。
· 可能出现异常
此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域。
虚拟机栈
· 作用
描述Java方法执行的内存模型。每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需的数据,成为栈帧,一个栈帧包含如:局部变量表、操作数栈、动态链接、方法出口等信息。
· 意义
JVM是基于栈的,所以每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程。
· 存储内容
局部变量表(编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息。
值得注意的是:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
· 可能出现的异常
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError(栈溢出错误)异常。
如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError(内存溢出错误)异常。
本地方法栈
· 作用
为JVM所调用到的Nativa即本地方法服务。
· 可能出现的异常
和虚拟机栈出现的异常很相像。
所有线程共有的数据区
Java堆
· 作用
所有线程共享一块内存区域,在虚拟机开启的时候创建。
· 意义
1、存储对象实例,更好地分配内存。
2、垃圾回收(GC)。堆是垃圾收集器管理的主要区域。更好地回收内存。
-存储内容
存放对象实例,几乎所有的对象实例都在这里进行分配。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。
值得注意的是:在JIT编译器[z1] [z2] 等技术的发展下,所有对象都在堆上进行分配已变得不那么绝对。有些对象实例也可以分配在栈中。
· 可能出现的异常
实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。
如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。
方法区
· 作用
用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
· 意义
对运行时常量池、常量、静态变量等数据做出了规定。
· 存储内容
运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
· 可能出现的异常
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
2,•加载.class文件的方式
–从本地系统中直接加载
–通过网络下载.class文件
–从zip,jar等归档文件中加载.class文件
–从专有数据库中提取.class文件
–将Java源文件动态编译为.class文件
将Java源文件动态编译为.class文件的理解:
简单理解:
就是在程序运行的时候,我们只有.java文件,没有已经编译好的.class文件,这时,在程序运行的时候,我们需要通过javax.tools. JavaCompiler接口获得系统编译器,并且通过它的run方法读取源代码,编译诊断,输出class,这叫动态编译。(系统运行之前就存在于磁盘等存储介质中的一个个文件,叫做静态的。系统运行的时候才会有的,通过系统的运行在内存中产生的,就叫动态。系统已经运行产生并放到了存储介质中的,叫静态的。)
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); int compilationResult = compiler.run(null, null, null, '/path/to/Test.java'); |
详解:
Java动态编译
程序产生过程
下图展示了从源代码到可运行程序的过程,正常情况下先编译(明文源码到字节码),后执行(JVM加载字节码,获得类模板,实例化,方法使用)。本文来探索下当程序已经开始执行,但在.class甚至.java还未就绪的情况下,程序如何获得指定的实现。这就是我们下面的主题,动态编译。
相关类介绍
JavaCompiler: 负责读取源代码,编译诊断,输出class
JavaFileObject: 文件抽象,代表源代码或者编译后的class
JavaFileManager: 管理JavaFileObject,负责JavaFileObject的创建和保存位置
ClassLoader: 根据字节码,生成类模板
使用方式
由于代码在编译的时候,类定义甚至类名称还不存在,所以没法直接声明使用的。只能定义一个接口代替之,具体实现留给后面的动态编译。
public
interface Printer {
public
void
print();
}
源代码的文件级动态编译
java源码以文件的形式存在本地,程序去指定路径加载源文件。
String classPath = File2Class.class.getResource(
"/").getPath();
//在这里我们是动态生成定义,然后写入文件。也可以直接读一个已经存在的文件
String str =
"import classloader.Printer;"
+
"public class MyPrinter1 implements Printer {"
+
"public void print() {"
+
"System.out.println(\"test1\");"
+
"}}";
//
将类的内容的字符串写入
MyPrinter1.java文件
FileWriter writer =
new FileWriter(classPath +
"MyPrinter1.java");
writer.write(str);;
writer.close();
//获得系统编译器,获取此平台提供的 Java™ 编程语言编译器工具。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获取一个标准文件管理器实现的新实例
StandardJavaFileManager fileManager = compiler.getStandardFileManager(
null,
null,
null);
//读入源文件,获取表示给定java文件的文件对象。
Iterable fileObject = fileManager.getJavaFileObjects(classPath +
"MyPrinter1.java");
//编译, 使用给定组件和参数创建编译任务。该编译可能没有完成。
JavaCompiler.CompilationTask task = compiler.getTask
[z3] (
null, fileManager,
null,
null,
null, fileObject);
task.call();
//
执行此编译任务
fileManager.close();
//指定class路径,默认和源代码路径一致,加载class,该类加载器用于从指向 jar文件和目录的 URL 的搜索路径加载类和资源。
//这里假定任何以 '/' 结束的 URL 都是指向目录的。
URLClassLoader classLoader =
new URLClassLoader(
new URL[]{
new URL(
"file:" + classPath)});
Printer printer = (Printer)classLoader.loadClass(
"MyPrinter1").newInstance();
//
加载目标
class
文件,并创建类的实例
printer.print();
源代码的内存级动态编译
上一节是通过java源文件动态编译加载的情况,这节让我们看下源代码和class全程都在内存中操作,如何实现动态编译。
思路:
是生成源代码对应的JavaFileObject时,从内存string读取;
生成class对应的JavaFileObject时,以字节数组的形式存到内存。
JavaFileObject是一个interface, SimpleJavaFileObject是JavaFileObject的一个基本实现,当自定义JavaFileObject时,继承SimpleJavaFileObject,然后改写部分函数。
1,自定义JavaSourceFromString,作为源代码的抽象文件(来自JDK API文档)
/**
*用于表示来自字符串的源的文件对象。
*/
public
class JavaSourceFromString extends SimpleJavaFileObject {
/**
*这个“文件”的源代码。
*/
final String code;
/**
*构造一个新的JavaSourceFromString。
* @param name此文件对象表示的编译单元的名称
* @param code该文件对象所代表的编译单元的源代码
*/
JavaSourceFromString(String name, String code) {
super(URI.create(
"string:///" + name.replace(
'.',
'/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
public CharSequence
getCharContent(
boolean ignoreEncodingErrors) {
return code;
}
}
2,JavaClassFileObject,代表class的文件抽象
public
class JavaClassFileObject extends SimpleJavaFileObject {
//用于存储class字节
ByteArrayOutputStream outputStream;
public
JavaClassFileObject(String className, Kind kind) {
super(URI.create(
"string:///" + className.replace(
'.',
'/') + kind.extension), kind);
outputStream =
new ByteArrayOutputStream();
}
public OutputStream
openOutputStream()
throws IOException {
return outputStream;
}
public
byte[]
getClassBytes() {
return outputStream.toByteArray();
}
}
3,ClassFileManager,修改JavaFileManager生成class的JavaFileObject的行为,另外返回一个自定义ClassLoader用于返回内存中的字节码对应的类模板
public
class ClassFileManager extends ForwardingJavaFileManager {
private JavaClassFileObject
classFileObject
;
/**
*创建ForwardingJavaFileManager的新实例。
*
* @param fileManager委托给这个文件管理器
*/
protected
ClassFileManager(JavaFileManager fileManager) {
super(fileManager);
}
/**
*获取要输出的JavaFileObject文件对象
*代表给定位置中指定类名的指定类别。
*/
@Override
public JavaFileObject
getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
FileObject sibling)
throws IOException {
classFileObject
=
new JavaClassFileObject(className, kind);
return classFileObject;
}
@Override
//获得一个定制ClassLoader,返回我们保存在内存的类
public ClassLoader
getClassLoader(Location location) {
return
new ClassLoader() {
@Override
protected Class<?>
findClass(String name)
throws ClassNotFoundException {
byte[] classBytes =
classFileObject
.getClassBytes();//
获取
class
文件对象的字节数组
return
super.defineClass(name, classBytes,
0, classBytes.length);//
定义
Class
对象
}
};
}
}
4,下面来偷梁换柱,用自定义的JavaFileObject/JavaFileManager来动态编译
String str = "import Printer;"
+ "public class MyPrinter2 implements Printer {"
+ "public void print() {"
+ "System.out.println(\"test2\");"
+ "}}";
//生成源代码的JavaFileObject
SimpleJavaFileObject fileObject = new JavaSourceFromString("MyPrinter2", str);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//被修改后的JavaFileManager
JavaFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));
//执行编译
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(fileObject));
task.call();
//获得ClassLoader,加载class文件
ClassLoader classLoader = fileManager.getClassLoader(null);
Class printerClass = classLoader.loadClass("MyPrinter2");
//获得实例
Printer printer = (Printer) printerClass.newInstance();
printer.print();
Java运行时动态生成class的方法(动态生成代理对象)
Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢?
因为在有些时候,我们还真得在运行时为一个类动态创建子类。
应用场景(问题)的产生:
比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?
以User
为例:
public
class User {
private String id;
private String name;
public String getId() {
return id;
}
public
void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public
void setName(String name) {
this.name = name;
}
}
其实UserProxy
实现起来很简单,就是创建一个User
的子类,覆写所有setXxx()
方法,做个标记就可以了:
public
class UserProxy extends User {
private
boolean dirty;
public
boolean isDirty() {
return
this.dirty;
}
public
void setDirty(
boolean dirty) {
this.dirty = dirty;
}
@Override
public
void setId(String id) {
super.setId(id);
setDirty(
true);
}
@Override
public
void setName(String name) {
super.setName(name);
setDirty(
true);
}
}
但是这个UserProxy
就必须在运行时动态创建出来了,因为编译时ORM框架根本不知道User
类。
解决方式一:自己动态生成字节码
现在问题来了,动态生成字节码,难度有多大?
如果我们要自己直接输出二进制格式的字节码,在完成这个任务前,必须先认真阅读JVM规范第4章,详细了解class文件结构。估计读完规范后,两个月过去了。
所以,第一种方法,自己动手,从零开始创建字节码,理论上可行,实际上很难。
解决方式二:使用已有的一些能操作字节码的库,帮助我们创建class。
目前,能够操作字节码的开源库主要有CGLib和Javassist两种,它们都提供了比较高级的API来操作字节码,最后输出为class文件。
比如CGLib,典型的用法如下:
Enhancer e =
new Enhancer();
e.setSuperclass(...);
e.setStrategy(
new DefaultGeneratorStrategy() {
protected ClassGenerator transform(ClassGenerator cg) {
return
new TransformingGenerator(cg,
new AddPropertyTransformer(
new String[]{
"foo" },
new Class[] { Integer.TYPE }));
}});
Object obj = e.create();
比自己生成class要简单,但是,要学会它的API还是得花大量的时间,并且,上面的代码很难看懂对不对?
有木有更简单的方法?
有!
解决方式三:动态生成.java文件,然后编译该文件,在加载编译后的.class文件
换一个思路,如果我们能创建UserProxy.java
这个源文件,再调用Java编译器,直接把源码编译成class,再加载进虚拟机,任务完成!
毕竟,创建一个字符串格式的源码是很简单的事情,就是拼字符串嘛,高级点的做法可以用一个模版引擎。
如何编译?
Java的编译器是javac
,但是,在很早很早的时候,Java的编译器就已经用纯Java重写了,自己能编译自己,行业黑话叫“自举”。从Java 1.6开始,编译器接口正式放到JDK的公开API中,于是,我们不需要创建新的进程来调用javac
,而是直接使用编译器API来编译源码。
使用起来也很简单:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compilationResult = compiler.run
[z4] (
null,
null,
null,
'/path/to/Test.java');
这么写编译是没啥问题,问题是我们在内存中创建了Java代码后,必须先写到文件,再编译,最后还要手动读取class文件内容并用一个ClassLoader加载。
有木有更简单的方法?
有!
其实Java编译器根本不关心源码的内容是从哪来的,你给它一个String
当作源码,它就可以输出byte[]
作为class的内容。
所以,我们需要参考Java Compiler API的文档,让Compiler直接在内存中完成编译,输出的class内容就是byte[]
。
代码改造如下:
Map<String,
byte[]> results;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(
null,
null,
null);
try (MemoryJavaFileManager manager =
new MemoryJavaFileManager(stdManager))
[z5] {
JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
CompilationTask task = compiler.getTask(
null, manager,
null,
null,
null, Arrays.asList(javaFileObject));
if (task.call()) {
results = manager.getClassBytes();
}
}
上述代码的几个关键在于:
1. 用自定义的MemoryJavaFileManager
替换JDK默认的StandardJavaFileManager
,以便在编译器请求源码内容时,不是从文件读取,而是直接返回String
;
2. 用自定义的MemoryOutputJavaFileObject
替换JDK默认的SimpleJavaFileObject
,以便在接收到编译器生成的byte[]
内容时,不写入class文件,而是直接保存在内存中。
最后,编译的结果放在Map<String, byte[]>
中,Key是类名,对应的byte[]
是class的二进制内容。
为什么编译后不是一个byte[]
呢?
因为一个.java
的源文件编译后可能有多个.class
文件!只要包含了静态类、匿名类等,编译出的class肯定多于一个。
如何加载编译后的class呢?
加载class相对而言就容易多了,我们只需要创建一个ClassLoader
,覆写findClass()
方法:
class MemoryClassLoader extends URLClassLoader {
Map<String,
byte[]> classBytes =
new HashMap<String,
byte[]>();
public MemoryClassLoader(Map<String,
byte[]> classBytes) {
super(
new URL[
0], MemoryClassLoader.class.getClassLoader());
this.classBytes.putAll(classBytes);
}
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf ==
null) {
return
super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf,
0, buf.length);
}
}
除了写ORM用之外,还能干什么?
可以用它来做一个Java脚本引擎。实际上本文的代码主要就是参考了Scripting项目的源码。
完整的源码呢?
在这里:https://github.com/michaelliao/compiler,连Maven的包都给你准备好了!compiler-master(动态创建class的代码).zip
也就200行代码吧!动态创建class不是梦!
3,类加载示意图:
•类的加载的最终产品是位于堆区中的Class对象
•Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
4,类加载器:Java虚拟机自带的类加载器和用户自定义的类加载器
•有两种类型的类加载器
–Java虚拟机自带的加载器
•根类加载器(Bootstrap)
•扩展类加载器(Extension)
•系统类加载器(System)
–用户自定义的类加载器
•java.lang.ClassLoader的子类
•用户可以定制类的加载方式,只要继承ClassLoader类和重写它的findClass方法就可以了。当然,也可以根据业务需求重写其它的相关方法。
5,类的加载时机和错误报告时机
•类加载器并不需要等到某个类被“首次主动使用”时再加载它,首次主动使用只是初始化的条件,不是加载的条件。我们可以手动调用类加载器的loadClass方法,主动加载想加载的类。
•JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError链接错误)
•如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
只有在首次主动使用被加载的类的时候,才会报告类在加载过程中发生的错误,LinkageError链接错误
类的链接:
•类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
1,类的验证:
•类的验证的内容
–类文件的结构检查
–语义检查
–字节码验证
–二进制兼容性的验证
2,类的准备:
3,类的解析:
类的初始化:
•类的初始化步骤
类的初始化时机(首次主动使用)
•主动使用(六种)
–创建类的实例
–访问某个类或接口的静态变量,或者对该静态变量赋值
–调用类的静态方法
–反射(如Class.forName(“com.shengsiyuan.Test”))
–初始化一个类的子类
–Java虚拟机启动时被标明为启动类的类(Java Test)
除了上述六种情形,其他使用Java类的方式都被看作是被动使用,不会导致类的初始化。
注意:
1, 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用,如果程序访问的静态变量或静态方法是在该类的父类或其他类中定义的,并不认为是对该类的主动使用也不会初始化该类。而是对定义这个静态变量或静态方法的类的主动使用,会初始化这个定义了该静态变量或方法的类。
2, 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
接口的初始化
只有当程序首次使用接口的静态变量时,才会导致该接口的初始化;
类加载器:
类加载器的作用:
Java虚拟机自带的3中类加载器:
根加载器默认加载rt.jar中的类,如果一个类是由根类加载器加载的,我们去获取这个类的类加载器对象,将返回一个空。因为根类加载器不对程序员暴露,java不允许程序员去获取根类加载器。
自定义类加载器:
类加载器的父子关系示意图:
类加载的父委托机制:
父委托机制:当某个类加载器要加载一个类时,他首先会去到自己的命名空间去查找这个类是否已经被加载,如果已经被加载,就返回这个类的Class对象的引用,如果没有被它加载,它不会马上去加载这个类,而是委托自己的父加载器去加载这个类,当然,父加载器也是先到自己的命名空间中去查找,看这个类是否已经加载,如果已经加载了,将这个类的Class对象的引用直接返回给到自己的子加载器,如果没有加载,继续向上委托,就这样,直到根加载器,如果根加载器在自己的命名空间中找到了这个类的Class对象,则证明根加载器已经加载了这个类,根加载器会将这个类的Class对象的引用返回给到自己的子加载器,这样一级一级返回,直到返回到要加载这个类的那个类加载器。如果根加载器还是没有加载过这个类,那么它会尝试去加载这个类,加载到了,就将这个类的Class对象的引用根据父子关系,依次接力向自己的子加载器返回,如果还是没有加载到,就会往下依次接力委托子加载器去加载,直到加载到这个类,然后返回这个类的Class对象的引用,依次传递给到要加载这个类的加载器。如果最终一直到要加载这个类的加载器都没有加载到这个类,那么程序抛出类找不到异常。
定义类加载器和初始类加载器的概念:
定义类加载器:成功加载了那个类的类加载器。那个类的Class对象是由它定义生成的,存放在它的命名空间中。
初始类加载器:定义类加载器以及它的所有子孙加载器都是初始类加载器。它们都能成功返回该类的Class对象的引用。
类加载器的父子关系的解释:
包装关系:类加载器是一个对象,而不是一个类。包装关系只得是一个对象里面持有另一个对象得引用,在类加载器的父子关系中,就是子加载器持有了父加载器的引用。父加加载器对象的实例通过子加载器的构造方法传入,如果不传,默认的自定义加载器的父加载器就是系统类加载器。
父委托机制的优点:
命名空间:
运行时包:
创建用户自定义的类加载器:
package com.shengsiyuan.classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class MyClassLoader extends ClassLoader { private String name; // 类加载器的名字 private String path = "d:\\"; // 加载类的路径 private final String fileType = ".class"; // class文件的扩展名 public MyClassLoader(String name) { super(); // 让系统类加载器成为该类加载器的父加载器 this.name = name; } public MyClassLoader(ClassLoader parent, String name) { super(parent); // 显式指定该类加载器的父加载器 this.name = name; } @Override public String toString() { return this.name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = this.loadClassData(name); return this.defineClass(name, data, 0, data.length); } private byte[] loadClassData(String name) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { this.name = this.name.replace(".", "\\"); is = new FileInputStream(new File(path + name + fileType)); baos = new ByteArrayOutputStream(); int ch = 0; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception ex) { ex.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception ex) { ex.printStackTrace(); } } return data; } public static void main(String[] args) throws Exception { MyClassLoader loader1 = new MyClassLoader("loader1"); loader1.setPath("d:\\myapp\\serverlib"); MyClassLoader loader2 = new MyClassLoader(loader1, "loader2"); loader2.setPath("d:\\myapp\\clientlib"); MyClassLoader loader3 = new MyClassLoader(null, "loader3"); loader3.setPath("d:\\myapp\\otherlib"); test(loader2); test(loader3); } public static void test(ClassLoader loader) throws Exception { Class clazz = loader.loadClass("Sample"); Object object = clazz.newInstance(); } } |
package com.shengsiyuan.classloader; public class Dog { public Dog() { System.out.println("Dog is loaded by : " + this.getClass().getClassLoader()); } } |
package com.shengsiyuan.classloader; public class Sample { public int v1 = 1; public Sample() { System.out.println("Sample is loaded by: " + this.getClass().getClassLoader()); new Dog(); } } |
不同类加载器的命名空间关系:
反射访问:不创建该类型的引用,直接通过该类的Class对象去获取该类的成员属性,方法等的描述对象,然后操作。
3,类的卸载:
由用户自定义的类加载器加载的类可卸载,由虚拟机自带的类加载器加载的类不可卸载;
示例:
[z1]JIT 是 just in time 的缩写, 也就是即时编译编译器。使用即时编译器技术,能够加速 Java 程序的执行速度。下面,就对该编译器技术做个简单的讲解。
首先,我们大家都知道,通常通过 javac 将程序源代码编译,转换成 java 字节码,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多。为了提高执行速度,引入了 JIT 技术。
在运行时 JIT 会把翻译过的机器码保存起来,以备下次使用,因此从理论上来说,采用该 JIT 技术可以接近以前纯编译技术。
[z2]JIT程序有两种运行方式:静态编译与动态解释。静态编译的程序在执行前全部被翻译为机器码,而解释执行的则是一句一句边运行边翻译。
把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。
参数:
Writer out - 用于来自编译器的其他输出的 Writer;如果为 null,则使用 System.err
JavaFileManager fileManager - 文件管理器;如果为 null,则使用编译器的标准文件管理器
DiagnosticListener<? super JavaFileObject> diagnosticListener - 诊断侦听器;如果为 null,则使用编译器的默认方法报告诊断信息
Iterable<String> options - 编译器选项;null 表示没有选项
Iterable<String> classes - 类名称(用于注释处理),null 表示没有类名称
Iterable<? extends JavaFileObject> compilationUnits - 要编译的编译单元;null 表示没有编译单元
返回:
表示编译的对象,编译任务对象
int run(InputStream in,
OutputStream out,
OutputStream err,
String... arguments)
使用给定 I/O 通道和参数运行工具。按照惯例,工具如果运行成功,则返回 0;如果出现错误,则返回非 0 值。任何生成的诊断都将以某种未指定的格式写入 out 或 err。
参数:
in - “标准”输入;如果为 null,则使用 System.in
out - “标准”输出;如果为 null,则使用 System.out
err - “标准”错误;如果为 null,则使用 System.err
arguments - 要传递给工具的参数,要编译的java文件。
返回:
如果成功,则返回 0;否则返回非 0 值
抛出:
NullPointerException - 如果参数数组包含任何 null 元素。
这是Try-with-resources结构。是java7中一个新的异常处理机制,它能够很容易地关闭在try-catch语句块中使用的资源。
应用场景:用于关闭资源。是一个能够确保资源能被正确地关闭的强大方法。
特点:try-with-resources 语句会确保在try语句结束时关闭所有资源。实现了java.lang.AutoCloseable或java.io.Closeable的对象都可以做为资源。
语法:将资源对象的创建写在try后面的括号中,括号中可以声明多个资源对象,用;号隔开,最后一个不用加分号。资源被关闭的顺序与它们被创建的顺序相反。
注意:try-with-resources 也可以有catch和finally语句块,就像使用一个普通的try语句一样。在try-with-resources 语句中,catch或者finally将在资源被关闭后执行。如果没有catch语句块,try中的异常将被抛出,但是从try-with-resources抛出的异常被禁止。在java7或更晚的版本中,我们可以获取到这些被禁止的异常。