-
JVM简明笔记3:类加载机制
1 类的加载
类的加载指的是将类的 .class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构。
类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被首次主动使用时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class
文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
加载 .class
文件的方式:
- 从本地系统中直接加载
- 通过网络下载.class文件
-
从zip,jar等归档文件中加载
.class
文件 -
从专有数据库中提取
.class
文件 -
将Java源文件动态编译为
.class
文件
执行顺序
-
类加载子系统负责从文件系统或是网络中加载
.class
文件,class文件在文件开头有特定的文件标识。 - 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
- 如果调用构造器实例化对象,则该对象存放在堆区;
2 类的生命周期
- 加载(Loading)
-
连接(Linking)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
加载阶段(Loading)
此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 java.lang.Class 对象,作为其他数据访问的入口。
(需要注意的是加载阶段和连接阶段的部分动作有可能是交叉执行的,比如一部分字节码文件格式的验证,在加载阶段还未完成时就已经开始验证了。)
-
预加载:虚拟机启动时加载,加载的是 JAVA_HOME/lib/ 下的 rt.jar 下的
.class
文件 (可以写一个空的 main 函数,设置虚拟机参数为-XX:+TraceClassLoading
来获取类加载信息) -
运行时加载:虚拟机在用到一个
.class
文件的时候,会先去内存中查看一下这个.class
文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。-
获取
.class
文件的二进制流 (没有规定二进制字节流要必须从哪里来或者怎么来,所以留下了可扩展的空间) -
将类信息、静态变量、字节码、常量这些
.class
文件中的内容放入方法区中 -
在内存中生成一个代表这个
.class
文件的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的
-
获取
验证阶段(Verification)
此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。
验证的主要动作大概有以下几个:
- 文件格式校验包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等;
- 元数据校验包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等;
- 字节码校验,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑;
- 符号引用校验,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。
准备阶段(Preparation)
此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。
HotSpot 虚拟机在 JDK 1.7 之前都在方法区,而 JDK 1.8 之后此变量会随着类对象一起存放到 Java 堆中。
- 这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
-
这个阶段赋初始值的变量指的是那些不被 final 修饰的 static 变量
-
比如
public static int value = 123
,在准备阶段过后是 0 而不是 123 ,给 value 赋值为123的动作将在初始化阶段才进行 -
比如
public static final int value = 123;
,在准备阶段,虚拟机就会给 value 赋值为123。
-
比如
解析阶段(Resolution)
此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。
- 符号引用:是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;
- 直接引用:是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。
初始化(Initialization)
初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。
3 Java类加载器
- 启动类加载器(BootStrapClassLoader):C/C++ 实现
- 其他类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader):Java 实现,规范定义自定义加载器是指派生于抽象类 ClassLoder 的类加载器
启动类加载器(Bootstrap ClassLoader)
C/C++ 实现,嵌套在 JVM 中,并不继承自 java.lang.ClassLoader,没有父加载器。用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar 或 sun.boot.class.path 路径下的内容),用于提供JVM自身需要的类。
扩展类加载器(Extension ClassLoader)
Java 实现,派生于 ClassLoader。由 sun.misc.Launcher$ExtClassLoader 实现。用来从 java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载。
应用类加载器(Application ClassLoader),又称系统类加载器
Java 实现,派生于 ClassLoader,父类加载器为扩展类加载器。由 sun.misc.Lanucher$AppClassLoader 实现。程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载的,它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库。通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。
用户自定义类加载器(User-Defined ClassLoader)
在日常的 Java 开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式。
类加载器包含示意图
ClassLoader 相关类图
重点关注 ClassLoader、ExtClassLoader、AppClassLoader。
4 双亲委派模型
过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。
好处:使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。同时也避免了多份同样字节码的加载。
Java 中实现双亲委派的代码都集中在 java.lang.ClassLoader
的 loadClass()
方法中,简单分析如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded // 1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 2. 判断一下是否有父加载器,若有交给父加载器加,否则调用 BootstrapClassLoader 加载。
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) { // 3. 如果第二步骤依然没有找到指定的类,那么调用当前类加载器的 findClass 方法来完成类加载。
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 自定义类加载器就可以重写 finalClass 方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
我们分析下 ExtClassLoader 与 AppClassLoader 源码,这两个类都是 sun.misc.Launcher
类的静态内部类
Launcher 类的初始化
/**
* This class is used by the system to launch the main application. Launcher类
*/
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
private static Launcher launcher = new Launcher(); // 1. Luncher 类加载后会从此静态变量调用到默认的构造方法
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader(); // 2. 扩展类加载器
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl); // 3. 应用类加载器(系统类加载器)
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// Also set the context class loader for the primordial thread. // 4. 设置 ContextClassLoader 为应用类加载器(系统类加载器)
Thread.currentThread().setContextClassLoader(loader);
// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
// init FileSystem machinery before SecurityManager installation
sun.nio.fs.DefaultFileSystemProvider.create();
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}
// ...
/*
* The class loader used for loading installed extensions. 关于 ExtClassLoader
*/
static class ExtClassLoader extends URLClassLoader {
// ...
private static ExtClassLoader createExtClassLoader() throws IOException {
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
final File[] dirs = getExtDirs(); // 4. 获取扩展类加载器加载的目录文件
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs); // 5. 创建一个扩展类加载器进行返回
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
void addExtURL(URL url) {
super.addURL(url);
}
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
SharedSecrets.getJavaNetAccess().
getURLClassPath(this).initLookupCache(this);
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs"); // 1. 将加载变量 java.ext.dirs 的值指示的路径下的类
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator); // 2. 按照 File.pathSeparator 进行 split
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs; // 3. 返回 File 对象数组
}
// ...
}
/**
* The class loader used for loading from java.class.path. 关于 AppClassLoader
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
// ...
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path"); // 1. 加载 java.class.path
final File[] path = (s == null) ? new File[0] : getClassPath(s);
// Note: on bugid 4256530
// Prior implementations of this doPrivileged() block supplied
// a rather restrictive ACC via a call to the private method
// AppClassLoader.getContext(). This proved overly restrictive
// when loading classes. Specifically it prevent
// accessClassInPackage.sun.* grants from being honored.
//
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
final URLClassPath ucp;
/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}
/**
* Override loadClass so we can checkPackageAccess. // 0. 重写 loadClass 方法:checkPackageAccess 之后调用 super.loadClass
*/
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
if (ucp.knownToNotExist(name)) {
// The class of the given name is not found in the parent
// class loader as well as its local URLClassPath.
// Check if this class has already been defined dynamically;
// if so, return the loaded class; otherwise, skip the parent
// delegation and findClass.
Class<?> c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
throw new ClassNotFoundException(name);
}
return (super.loadClass(name, resolve));
}
// ...
}
}
5 自定义类加载器
自定义类加载器的目的:
- 隔离加载类:模块隔离,把类加载到不同的应用选中。比如 Tomcat 这类 Web 应用服务器,内部自定义了好几中类加载器,用于隔离 Web 应用服务器上的不同应用程序。
- 修改类加载方式:除了 Bootstrap 加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。
- 扩展加载源:可以实现从其他途径加载 class 文件。
- 防止源码泄漏:Java 代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。
通过对双亲委派模型源码的解读,我们可以分析出两种自定义类加载器的做法:
-
重写
loadClass
方法:不推荐,因为会破坏双亲委派模型 -
重写
findClass
方法:推荐
采用 findClass
方法自定义类加载器实现:
public class MyClassLoader extends ClassLoader{
private String dir;
public static final String fileType = ".class";
public MyClassLoader(String dir) {
this.dir = dir;
}
public MyClassLoader(ClassLoader parent, String dir) {
super(parent);
this.dir = dir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 定义输入和输出流
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
String fileName = dir + name + fileType;
try {
bis = new BufferedInputStream(new FileInputStream(fileName));
baos = new ByteArrayOutputStream();
// 读取字节数据
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
byte[] byteCode = baos.toByteArray();
// 字节数组转为Class对象
Class<?> definedClass = defineClass(null, byteCode, 0, byteCode.length);
return definedClass;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
测试
public class MyClassLoaderTest {
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader("D:/");
try {
Class<?> dogClass = classLoader.loadClass("Test");
System.out.println(dogClass.getClassLoader().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
内容为之前学习笔记整理,如果有问题请指正!
来源:
https://www.cnblogs.com/linweiwang/p/15496765.html