JVM02_类加载子系统
Posted root_zhb
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM02_类加载子系统相关的知识,希望对你有一定的参考价值。
文章目录
1、类加载过程
在Java中数据类型分为基本数据类型和引用数据类型。
基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载
加载(Loading)
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
生成的Class对象的位置(HotSpot虚拟机):
JDK1.7中,在方法区或者说永久代中
JDK1.8中,在方法区或者说元空间中
注意
【方法区其实只是个虚拟的概念,方法区具体的实现是永久代或者元空间,1.7是永久代,1.8是元空间】
永久代和元空间最大的区别:JDK7的永久代放在堆中并且独立于堆,JDK8的元空间完全剥离虚拟机,存在于直接内存中
怎么理解Class对象与new出来的对象之间的关系?
每个类都对应有一个Class类型的对象,多个new出来的实例。
每个new出来的对象都是以Class类为模板参照出来的。
为什么可以参照?因为Class对象提供了访问方法区内的数据结构的接口(访问入口)
链接 (Linking)
验证(Verification)
- 确保Class文件的字节流中包含信息符合当前虚拟机要求,确保被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证:文件格式验证(魔术oxCAFEBABE),元数据验证,字节码验证,符号引用验证
准备(Preparation)
- 为类变量分配内存,并将其初始化为默认值;8中基本类型的默认值+引用类型的null
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式赋值
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中
解析(Resolution)
- 将常量池中的符号引号转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
- JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。将其在常量池中的符号引用替换成直接其在内存中的直接引用。
- 符号引号有:类和接口的权限定名、字段的名称和描述符、方法的名称和描述符
初始化(Initialization)
- 为类的静态变量赋予正确的初始值
- 初始化阶段就是执行类构造器方法<clinit>()的过程。此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 若该类具有父类,Jvm会保证子类的<clinit>() 执行前,父类的<clinit>() 已经执行完成。
public class ClinitTest1 {
static class Father{
public static int A=1;
static{
A=2;
}
}
static class Son extends Father{
public static int B=A;
}
public static void main(String[] args) {
//这个输出2,则说明父类已经全部加载完毕
System.out.println(Son.B);
}
}
- Java编译器并不会为所有的类都产生()初始化方法。场景如下:
//哪些场景下,java编译器就不会生成<clinit>()方法
public class InitializationTest1 {
//场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public int num = 1;
//场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
public static int num1;
//场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public static final int num2 = 1;
}
- static与final的搭配问题
(使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行)
/**
* 说明:使用static + final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
* 情况1:在链接阶段的准备环节赋值
* 情况2:在初始化阶段<clinit>()中赋值
* 结论:
* 在链接阶段的准备环节赋值的情况:
* 1. 对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
* 2. 对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
*
* 在初始化阶段<clinit>()中赋值的情况:
* 排除上述的在准备环节赋值的情况之外的情况。
* 最终结论:使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。
*/
public class InitializationTest2 {
public static int a = 1;//在初始化阶段<clinit>()中赋值
public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段<clinit>()中赋值
public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1");//在初始化阶段<clinit>()中赋值
public static String s2 = "helloworld2";
public static final int NUM1 = new Random().nextInt(10);//在初始化阶段<clinit>()中赋值
}
6、clinit()的调用会死锁吗?
1、虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕
2、正是因为函数()带锁线程安全的,因此,如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}
public class StaticDeadLockMain extends Thread {
private char flag;
public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}
@Override
public void run() {
try {
Class.forName("Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}
public static void main(String[] args) throws InterruptedException {
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}
2、类加载器及其分类和测试
四者之间是包含关系,不是上层下层,也不是子父类的继承关系
-
启动类加载器,Bootstrap ClassLoader
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容),用于提供JVM自身需要的类(String类就是使用的这个类加载器) - 由于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
-
扩展类加载器,Extension ClassLoader
- Java语言编写,由sum.music.Launcher$ExtClassLoader实现
- 派生于ClassLoader类,父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
-
应用程序类加载器,Application ClassLoader
- java语言编写,由sum.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类,父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
-
自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类
- 定制类的加载方式(自定义类加载器通常需要继承于 ClassLoader)
- 实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源
- 如何自定义:
继承Java.lang.ClassLoader;
1.2之前重写loadClass(),1.2之后建议把自定义的类加载逻辑写在findClass()里,根据参数指定类的名字,返回对应的Class对象的引用;
无复杂需求,继承ClassLoader子类URLClassLoader,避免重写findClass()及其获取字节码流的方式。
自定义类加载器示例
public class UserClassLoader extends ClassLoader {
private String rootDir;
public UserClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 编写findClass方法的逻辑
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//直接生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 编写获取class文件并转换为字节码流的逻辑 * @param className * @return
*/
private byte[] getClassData(String className) {
// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
// 读取类文件的字节码
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 类文件的完全路径
*/
private String classNameToPath(String className) {
return rootDir + "\\\\" + className.replace('.', '\\\\') + ".class";
}
public static void main(String[] args) {
//此处为class文件所在目录
String rootDir = "D:\\\\testJVM\\\\out\\\\production\\\\testJVM\\\\";
try {
//创建自定义的类的加载器1
UserClassLoader loader1 = new UserClassLoader(rootDir);
Class clazz1 = loader1.findClass("User");
//创建自定义的类的加载器2
UserClassLoader loader2 = new UserClassLoader(rootDir);
Class clazz2 = loader2.findClass("User");
//clazz1与clazz2对应了不同的类模板结构
System.out.println(clazz1 == clazz2);
System.out.println(clazz1.getClassLoader());
System.out.println(clazz2.getClassLoader());
Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("User");
System.out.println(clazz3.getClassLoader());
System.out.println(clazz1.getClassLoader().getParent());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果
false
UserClassLoader@131245a
UserClassLoader@15fbaa4
sun.misc.Launcher$AppClassLoader@dad5dc
sun.misc.Launcher$AppClassLoader@dad5dc
分类:
在JVM规范中,加载器分为两类:
启动类加载器:使用C/C++语言实现的,嵌套在JVM内部
自定义类加载器:将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
测试:
- 每个Class对象都会包含一个定义它的ClassLoader的一个引用
- 获取ClassLoader的途径
//(1). 获得当前类的ClassLoader
clazz.getClassLoader()
//(2). 获得当前线程上下文的ClassLoader(系统类加载器)
Thread.currentThread().getContextClassLoader()
//(3). 获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
- 数组类的Class对象,不是由类加载器去加载的,而是在Java运行期JVM根据需要自动创建的。对于数组的类加载器来说,是通过Class.getClassLoader()返回的,与数组中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的(基本数据类型由虚拟机预先定义)
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader classloader1 = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@dad5dc
System.out.println(classloader1);
//获取到扩展类加载器
//sun.misc.Launcher$ExtClassLoader@131245a
System.out.println(classloader1.getParent());
//获取到引导类加载器 null
System.out.println(classloader1.getParent().getParent());
//获取系统的ClassLoader
ClassLoader classloader2 = Thread.currentThread().getContextClassLoader();
//sun.misc.Launcher$AppClassLoader@dad5dc
System.out.println(classloader2);
String[]strArr=new String[10];
ClassLoader classLoader3 = strArr.getClass().getClassLoader();
//null,表示使用的是引导类加载器
System.out.println(classLoader3);
ClassLoaderDemo[]refArr=new ClassLoaderDemo[10];
//sun.misc.Launcher$AppClassLoader@dad5dc
System.out.println(refArr.getClass().getClassLoader());
int[]intArr=new int[10];
//null,如果数组的元素类型是基本数据类型,数组类是没有类加载器的
System.out.println(intArr.getClass().getClassLoader());
}
}
3、ClassLoader源码剖析
- ClassLoader与现有类加载器的关系
- 抽象类ClassLoader的主要方法(内部没有抽象方法)
方法名 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例。 |
findClass (String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例。 |
resolveClass(Class<?> c) | 链接(Linking)指定的一个Java类。 |
Classloader源码loadClass方法源码
//resolve==true,加载class的同时需要进行解析操作,一般为false
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//同步操作,保证只能加载一次
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 在缓存中判断是否已经加载同名的类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//获取当前类的父类加载器
if (parent != null) {
//如果存在父类加载器,则调用父类加载器进行类的加载(双亲委派机制)
c = parent.loadClass(name, false);
} else {
//parent==null 父类加载器是引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 调用当前classloader的findClass
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;
}
}
-
SecureClassLoader与URLClassLoader
- SecureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联
- ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,无复杂需求,一般继承URLClassLoader类。
-
Class.forName()与ClassLoader.loadClass()对比
- Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。
例如:数据库中的初始化驱动Class.forName(“com.mysql.jdbc.Driver”); - ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。
- Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。
4、双亲委派机制
工作原理:
- 如果一个类加载收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
- 如果父类的加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
源码体现
双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现,可查看3.2中的源码
- 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
- 判断当前加载器的父加载器是否为空,如果不为空,则调
以上是关于JVM02_类加载子系统的主要内容,如果未能解决你的问题,请参考以下文章
jvm系列-02jvm的类加载子系统以及jclasslib的基本使用