* [2.2.2,准备](#222_55)
* [2.2.3,解析](#223_68)
- [2.3,初始化阶段(重点)](#23_77)
- * [2.3.1,jclasslib的安装](#231jclasslib_79)
* [2.3.2,clinit](#232clinit_96)
* [2.3.3,init](#233init_118)
+ [3,类加载器](#3_125)
+ - [3.1,类加载器的分类](#31_127)
- [3.2,自定义类加载器的场景](#32_176)
- [3.3,双亲委派机制](#33_200)
- [3.4,自定义类加载器](#34_248)
- [3.5,打破双亲委派模型](#35_304)
- [3.6,其他](#36_307)
1,jvm的内存结构
在jvm的内存中结构中,其主要结构如下。
在jvm内部,需要将磁盘上的字节码文件通过这个类加载加载到内存中。在类加载子系统中,也需要经过一定的阶段将才能将这个文件加载到内存的运行时数据区中,如一些加载,验证,准备,解析,初始化等工作。在加载到运行时数据区之后,内部主要由一些共享的方法区、堆,以及私有的程序计数器、虚拟机栈、本地方法栈这些。这些字节码最终是需要通过执行引擎去执行的,执行引擎中主要包括解释器,JIT即时编译器,垃圾回收器等。
2,类加载器加载过程
在类加载器子系统中,主要会经过加载,链接和初始化三个阶段,链接又包括验证,准备和解析三个阶段,所以合起来就是加载,验证,准备,解析,初始化五个阶段。
类加载器主要负责从文件系统或者网络中加载Class文件,并且类加载器只负责将文件加载,至于是否可以运行,还得由Execution Engine执行引擎决定。
2.1,加载阶段
加载阶段的加载器主要有引导类加载器,扩展类加载器,系统类加载器和自定义类加载器,主要是通过一个类的全限定名获取此类的二进制字节流,然后将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构,然后在内存中生成一个java.lang.Class文件,作为方法区这个类的各种数据的访问入口。其主要就是将文件加载出来
常见的类加载方式有以下几种方式
- 从本地系统直接加载
- 从网络中获取
- 从压缩包中获取,如zip
- 运行时生成,如动态代理
- 其他文件生成,典型的场景有:JSP应用
- 数据库中获取 .class文件
- 从加密文件中获取
- 反射,序列化,克隆等
2.2,链接阶段
链接阶段又可以分为三个阶段,分别是验证,准备和解析
2.2.1,验证
验证的主要目的在于确保Class文件的字节流中所包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全,相当于一种自我保护。如果编译器发现了有违法的信息之后,则编译器可以选择直接抛出异常或者拒绝编译。
主要包括四种验证:文件格式验证,元数据验证,字节码验证和符号引用验证。
2.2.2,准备
在准备阶段为类分配内存,并且设置该类的变量默认初始值,如整型的初始值为0。
public static int x = 10; //在准备阶段赋值默认值为0,并且分配内存
public static void main(String[] args) {
System.out.println(j);
}
这里主要是为变量进行一个默认的初始赋值,如果变量被static final修饰,那么这个变量会被变为常量,并且会在编译阶段就会分配内存。同时这里也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
2.2.3,解析
就是将常量池内的符号引用转化为直接引用的过程,并随着JVM在执行完初始化之后再执行。
符号引用:以一组符号来描述引用的目标,只要能无歧义的定位到目标即可
直接引用: 相当于寻址的直接指针或者句柄
解析动作主要针对接口,类,字段,类方法,接口方法,方法类型,句柄和调用点限定符
2.3,初始化阶段(重点)
2.3.1,jclasslib的安装
在查看字节码文件之前,也可以在idea中安装查看对应的字节码指令的插件,在插件中搜索jclasslib即可,安装完成之后需要restart重启。
在安装完成之后,可以在view的位置来打开这个Bytecode字节码文件。
在点击这个Show Bytecode With Jclasslib 之后,就会出现以下的界面,会有一些版本,协议号,当前类,父类,接口数,文件数,方法数,属性数等。
2.3.2,clinit
初始化阶段就是执行类构造器方法()的过程,通过javac编译器自动收集类中的所有类变量赋值动作和静态代码块中的语句合并而来的。就是说这个clinit会将类变量的显示的初始化和静态代码块的初始化合并到一起,如果没有类变量的赋值操作或者静态代码块的赋值操作,那么这个clinit就不会出现在字节码文件中。
并且在整个流程中,变量的初始赋值是在这个准备阶段,而真正的赋值是在这个初始化阶段。
public static int x = 10; //当前阶段中此时x的值为10
在这个Methods中,可以看到给这个类变量赋值,是有这个clinit的
或者再静态代码块中给类变量赋值,也是可以有这个clinit的,可以看下图右边Methods中的第二点。
如果该类具有父类,那么JVM会保证先加载父类的 ,再加载子类的 。并且在多线程中,虚拟机会保证一个类的 方法会加同步锁
2.3.3,init
在每个类中,都会有一个隐示的构造方法或者显示的构造方法,通过 来进行初始化。如在以下的代码中,显示的写了一个代码的构造器,先将初始值加载,或者再加载构造器里面的值。
3,类加载器
3.1,类加载器的分类
在加载阶段中,主要有引导类加载器,扩展类加载器,应用程序类加载器和自定义加载器。在jvm中,规定支持两种类加载器,分別是引导类加载器和自定义类加载器,而扩展类和系统类都是属于自定义类加载器。
并且在这几个来加载器中,这个引导类加载器是用c语言写的,而其他的类加载器都是使用这个JAVA语言写的。接下来通过代码查看一下这个类加载器,也可以发现这个引导类加载器不是java语言写的,所以获取不到,并且这个自定义类的加载器是通过系统类加载器来加载的。
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(systemClassLoader);
//获取上层扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
//sun.misc.Launcher$ExtClassLoader@15615099
System.out.println(extClassLoader);
//获取上层引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
//null 尝试获取失败,该类由c语言编写
System.out.println(bootStrapClassLoader);
//获取自定义类类加载器,以当前类为例
ClassLoader classLoader = ClassLoad.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
// 可以发现当前自定义类的了地价在其为系统类加载器
System.out.println(classLoader);
而像一些系统的核心类库,如String这种,是通过引导类加载器加载的。并且该加载器作为扩展类和系统类加载器的父类加载器,该加载器主要加载包名为java,javax,sun等开头的类
ClassLoader StringClassLoader = String.class.getClassLoader();
System.out.println(StringClassLoader); //null
接下来可以获取一下这个引导类中,加载的全部内容
//获取引导类加载器可以加载的全部url
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
System.out.println(urLs[i]);
}
3.2,自定义类加载器的场景
一般情况使用引导类,扩展类和系统类是可以满足日常的开发需求的,但是在必要时,也可以手动自定义其他的类加载器。
引入自定义类加载器的原因
- 隔离加载类
- 修改类加载方式
- 扩展加载源
- 防止源码泄漏
自定义类加载器的实现步骤
- 1,可以通过继承抽象类 java.lang.ClassLoader 类,实现自定义类加载器
- 2,重写findClass()方法,然后将逻辑写在方法内部
- 3,如果没有特别复杂的要求,可以直接继承URLClassLoader类
获取ClassLoader的途径
- 获取当前类的ClassLoader:clazz.getClassLoader()
- 获取上下文线程方式:Thread.currentThread.getContextClassLoader()
- 获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
- 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()
3.3,双亲委派机制
在jvm中,对class文件采用的是按需加载的方式,也就是说在需要使用到该类时才会加载,然后将class文件加载到内存生成class对象。并且java虚拟机采用的是一种双亲委派机制模式
其工作原理如下:
- 1,如果一个类加载器收到了加载请求,他并不会自己去加载,而是将这个请求委托给父类加载器去执行
- 2,如果父加载器还有其他的父加载器,那么会进一步的向上委托,一次递归到顶点
- 3,如果父类可以完成任务,则将值返回;反之,则由子类尝试去加载
其源码如下
// 检查当前类加载器是否已经加载了该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //如果当前加载器父加载器不为空则委托父加载器加载该类
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
c = findClass(name);
通过源码也可以知道:
1,首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
2,如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新