JVM内存模型(一)------ 类加载子系统
标签: JVM内存模型(一)------ 类加载子系统 Java博客 51CTO博客
2023-07-11 18:24:14 124浏览
JVM内存模型如下,本系列就是按照下图进行研究的:
类加载子系统
1、类加载子系统负责从文件系统或者是网络中加载Class文件,Class文件在文件开头有特定的文件标识;
2、ClassLoader
只是负责Class文件加载,至于他是否可以运行,则由ExecuteEngine
(执行引擎)决定;
3、类加载的信息通过ClassLoader
加载了之后,类的信息会被加载到成为方法区的内存空间。
类加载过程
加载(Loading)
- • 通过类的全限类名定义获取此类的二进制字节流
- • 将这个字节流所代表的二进制存储结构转化为方法区的运行时数据结构
- • 在内存中生成一个代表这个类
java.lang.Class
对象,作为方法区这个类的各种数据访问入口。
链接(Linking)
- • 验证(Verification)
- • 保证Class文件的字节流中包含的信息符合当前虚拟机的要求,保证加载类的正确性,不会危及虚拟机的自身安全
- • 准备(Preparation)
• 为类变量分配内存并设置该类变量的默认初始值,即零值。
• 这里不包含final修饰的static值 ,因为final在编译的时候就被分配了
• 这里不会为实例变量分配初始化 ,类变量会分配到方法区中,而实例变量会随着对象一起分配到Java堆中。
• 解析(Resolution)
• 将常量池内的符号引用转化为直接引用的过程
初始化(Initialization)
- • 初始化阶段就是执行类构造方法<clinit>()的过程。
- • 该方法不需要定义,是javac编译器自动收集类中的所有变量的赋值动作和静态代码块中的语句合并而来。
- • <clinit>()方法不同于类的构造器
- • 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经加载完毕。
- • 虚拟机必须保证一个类的<clinit>()方法在多线程环境下同步加锁执行
类加载器
主要分为两类,第一类就一个,引导类加载器(BootstrapClassLoader
),第二类为所有派生于抽象类ClassLoader
的类加载器,被称之为自定义类加载器。
这两类类加载器的工作原理是一样的,区别是它们的加载路径不同,主要作用的话是为了保证对象加载的唯一性以及安全性。
类加载器工作流程:
引导类加载器(BootStrapClassLoader
)
- • 使用C/C++语言实现的,嵌套在JVM内部;
- • 主要用于加载Java的核心类库
- • 并不继承与
java.lang.ClassLoader
,没有父加载器 - • 出于安全考虑,
Bootstrap
类加载器只会加载包名为java,javax,sun
等开头的类。
该类加载器主要加载的位置有:
public static void main(String[] args) {
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
Stream.of(urLs).forEach(e-> System.out.println(e.toExternalForm()));
}
---------输出BootStrapClassLoad加载器加载的位置
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/classes
自定义类加载器(User Defined ClassLoader
)
自定义类加载器都是派生于ClassLoader
接口:sun.misc.Launcher
。他是java虚拟机的一个入口应用,自定义类加载器位于内部作为一个内部类在使用。
虚拟机自带的类加载器
- 1. 扩展类加载器(
ExtClassLoader
)
- • java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现; - • 加载
\jre\lib\ext
下面的Jar
等 - • 派生于
ClassLoader
- • 父类加载器为启动类加载器
扩展类加载器主要加载的位置:
public static void main(String[] args) {
String property = System.getProperty("java.ext.dirs");
Stream.of(property.split(":")).forEach(System.out::println);
}
---------输出ExtClassLoad加载器加载的位置
/Users/babyshi/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
- 1. 应用程序类加载器(
AppClassLoader
)
- • Java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现; - • 派生于ClassLoader;
- • 父类为扩展类加载器;
- • 他负责加载环境变量
classpath
或者系统属性,java.class.path
指定路径下的类库; - • 该类是程序中默认的类加载器,一般来说,Java中的应用类都是由他来进行加载的;
- • 通过ClassLoader.getSystemClassLoader()可以获得到该类加载器。
自定义的类加载器
- 1. 为什么要自定义类加载器?
- • 隔离加载类
- • 修改类的加载方式
- • 扩展加载源
- • 防止源码泄露
- 1. 自定义步骤
- • 继承抽象类
java.lang.ClassLoader
类的方式,实现自己的类加载器,以满足一些特殊的要求; - • 在jdk1.2之前,继承
ClassLoader
并重写loadClass()
方法,jdk1.2之后,不再建议重写loadClass()
方法,而是建议把自定义的类加载逻辑写在findClass()
方法中; - • 编写自定义类加载器时,如果没有复杂的要求,可以直接继承
URLClassLoader
类。ClassL oader类国 ClassLoader获取途径国
ClassLoaader
获取方式
//方式一:获取当前类的ClassLoader
clazz.getClassLoader()
//方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
//方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
//方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
双亲委派机制
Java虚拟机对Class文件采用的是按需加载方式,也就是说需要这个类的时候才会将他的Class文件加载到内存生成Class对象,而且加载某个类的Class文件的时候,Java虚拟机采用的是双亲委派模式 ,即把请求交给父类处理,他是一种任务委派模式。
原理
工作原理:
- • 如果一个类加载器收到类加载请求的时候,他并不会自己先去加载,而是把这个请求委托给父类加载器去执行;
- • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求到达最顶层的启动类记载器;
- • 如果父类加载器可以完成类的加载任务,就成功返回,如果父类加载器无法完成此加载任务,于是子加载器才会尝试自己去加载,这就是双亲委派模式。
实例一:
实例二:
我们来看看ClassLoader
抽象类中的加载Class
的模版代码:
public abstract class ClassLoader {
// ....
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1、检查该class对象是否已经被加载
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
// 没有加载过的话进行加载Class
if (c == null) {
long t0 = System.nanoTime();
try {
// 存在父类加载器则调用父类的类加载器进行加载(此处是递归调用)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 如果父加载器没加载成功,调用自己的findClass去加载
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
// 通过类名称查询 调用的native方法
return findLoadedClass0(name);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
// 空实现,子类实现,主要是通过传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
}
}
从代码我们我们可以看出:
- • 类加载器是分层次的,它们是父子关系,每个类加载器都持有一个
parent
字段,指向父加载器 - •
findClass
方法的主要职责就是找到.class
文件,然后将文件读取到内存中
所以得到的类加载器工作流程:
- • 先检查该对象是否已经加载过了,加载过的话直接返回
- • 如果未加载过,则校验是否存在父类加载器,存在则委托父类加载器加载,如果不存在父类加载器,则使用
BootstrapClassLoader
加载 - • 如果父类加载失败,则调用自己的
findClass
方法进行加载对象
优势
使用双亲委派机制的优势:
- • 避免类的重复加载
- • 保护程序安全,防止核心api被篡改
- • 比如自定义一个类
java.lang.String
-------也就是自己创建一个java.lang
包,下面再创建一个类String,该情况由于双亲委派机制的存在,不会执行自定义类,而是执行系统自带的String类 - • 比如自定义一个类
java.lang.TestApp
-------虽然系统不存在该类,但是由于存在于java.lang
包下,就会交给引导类加载器(BootrapClassLoader
),他识别该类并向外抛出异常(java.lang.SecurityException:Prohibited package name : java.lang
),以保证安全性。
沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\string.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
如自定义如下类并放在java.lang
包下面
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("hello Word");
}
}
---------------报错内容如下:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
JVM判断两个类是同一个对象的条件
- • 类的完整类名必须一致,包括包名;
- • 加载这个类的类加载器必须完全相同。
换句话来说,在JVM中,即使两个类的对象(Class对象)来源于同一个Class文件,被同一个虚拟机加载,但是只要是加载他们的ClassLoader不同,那么这两个对象也就是不相等的。
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论