一个类的完整生命周期如下:
Class文件在我们硬盘中,需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
详见:jvm规范5.4 。
类加载过程的第一步,主要完成下面 3 件事情:
Class
对象,作为方法区这些数据的访问入口 虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:“通过全类名获取定义此类的二进制字节流” 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP
包中读取(日后出现的 JAR
、EAR
、WAR
格式的基础)、其他文件生成(典型应用就是 JSP
)等等。
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass()
方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
总结来说:Loading 是将本地的classfile的二进制的内容加载到内存中。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
Verification 的主要过程就是用来校验,如何加载的文件的字节码头不是CAFEBABE,该过程就会被拒绝。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
static
关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。public static int value=111
,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111
,那么准备阶段 value 的值就被赋值为 111。 基本数据类型的零值 : (图片来自《深入理解 Java 虚拟机》第 3 版 7.33 )
所以说Preparation过程的主要作用就是将静态变量赋默认值,假设定义public static int i=8;在这个过程中并不是把i的值赋值成8,而是要对静态变量i进行默认值的赋值也就是0。
**解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。**解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。也就是说该过程 将class文件中常量池用到的一些符号引用转换为内存地址。
初始化阶段是执行初始化方法
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
说明:
方法是编译之后自动生成的。
()
对于
方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为
方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。
对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
(1)当遇到 new
、 getstatic
、putstatic
或 invokestatic
这 4 条直接码指令时,比如 new
一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
new
指令时会初始化类。即当程序创建一个类的实例对象。getstatic
指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。putstatic
指令时会初始化类。即程序给类的静态变量赋值。invokestatic
指令时会初始化类。即程序调用类的静态方法。(2)使用 java.lang.reflect
包的方法对类进行反射调用时如 Class.forname("...")
, newInstance()
等等。如果类没初始化,需要触发其初始化。
(3)初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
(4)当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main
方法的那个类),虚拟机会先初始化这个类。
(5)MethodHandle
和 VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 findStaticVarHandle
来初始化要调用的类。
(6)「补充,来自issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
总结:静态变量在该步骤下进行赋值为初始值,才会调用静态代码块。
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象;
该类没有在其他任何地方被引用;
该类的类加载器的实例已被 GC。
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了,jdk 自带的 BootstrapClassLoader
, ExtClassLoader
, AppClassLoader
负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
JVM本身有个类加载器的层次,这个类加载器就是普通的Class,它的层次就是用来加载不同的各种各样的class,下图是类加载器的详细图解:
最顶层 BootstrapClassLoader(启动类加载器)
最顶层的加载类,由 C++实现,是用来加载lib/rt.jar charset.jar等核心类 C++实现。主要负责加载jdk中最核心的jar包和类或者被 -Xbootclasspath
参数指定的路径中的所有类 ,例如runtime.jar 或者是我们平时所说的String.class,Object.class 都是位于lib/rt.jar 。当出现null值,就是调用的最顶层加载器,在java的类中没有这样的对象去应对他。
第二层 ExtClassLoader(扩展类加载器)
ExtClassLoader是用来加载扩展的jar包和类,主要负责加载 %JRE_HOME%/lib/ext
目录下的 jar 包和类,或被 java.ext.dirs
系统变量所指定的路径下的 jar 包。用jre/lib/ext/*.jar 或由-Djava.ext.dirs指定。
第三层 AppClassLoader(应用程序类加载器)
AppClassLoader是用来加载classpath指定的内容。面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
第四层 自定义加载器
自定义加载器是用来加载自定义的类的内容。
在这里说到类加载器,也不得不说到类加载器的加载过程是如何的,类加载器的加载过程称作双亲委派。
在双亲委派中存在一个概念,叫父加载器。注意这里的父加载器不是继承关系,是由“优先级”来决定,如下图所示:
该图描述的是语法上一种继承关系,而继承关系和父加载器没关系。
父加载器其实指代的是 ClassLoader 源码中有一个变量,这个变量是Classloader类型,名称叫parent,双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,如下源码所示:
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
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;
}
}
每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
上述已经基本介绍过双亲委派了,下面进行详细说明双亲委派的具体过程,结合下图所示,可以更好的理解双亲委派的整个过程:
首先Class文件通过自定义的classloader进行加载,如果它没有加载,那么则委托它的父加载器Appclassloader加载,Appclassloader 判断是否为本地加载,如果有则直接加载,如果没有则继续向上委托,直到顶层的加载器BootstrapClassLoader,但是当顶层的加载器,也没有加载时,就会向下委托,当所有的下级加载器都没有加载那么则抛出异常 classNotFound 异常,如果下级加载器能够加载,那么就由下级加载器进行加载。
**所以总结来说就是自底向上检查这个类是否被加载,如果到最顶层也都没有加载,则自顶向下尝试加载类,就相当于一种委托。**所以说可以这么去理解双亲委派:
双亲委派这部分执行的代码是被写死的,如上述源码所示。
首先第一父加载器不是类的加载器的加载器,也不是加载器的父类的加载器。
父加载器其实是指的 ClassLoader 源码中有一个变量,这个变量是Classloader类型,名称叫parent。
每个类加载都有一个父类加载器,我们通过下面的程序来验证。
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}
}
输出:
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is null
AppClassLoader
的父类加载器为ExtClassLoader
,
ExtClassLoader
的父类加载器为 null,null 并不代表ExtClassLoader
没有父类加载器,而是 BootstrapClassLoader
。
package com.openlab;
public class ClassLoaderTest02 {
public static void main(String[] args) {
System.out.println(ClassLoaderTest02.class.getClassLoader());
System.out.println(ClassLoaderTest02.class.getClassLoader().getClass().getClassLoader());
System.out.println(ClassLoaderTest02.class.getClassLoader().getParent());
System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent());
}
}
输出结果:
从上个案例的执行结果中,我们可以看出Appclassloader和Extclassloader 都是Launcher的内部类。 Launcher是classloader的包装类启动类,null 并不代表没有类加载器,而是 BootstrapClassLoader
。
在Launcher源码中:
private static String bootClassPath = System.getProperty("sun.boot.class.path");
final String var1 = System.getProperty("java.class.path");
String var0 = System.getProperty("java.ext.dirs");
sun.boot.class.path 是BootstrapClassloader的加载路径;
java.class.path 是AppClassloader的加载路径;
java.ext.dirs 是ExtClassLoader的加载路径;
所以进行测试,通过路径查看类加载器的范围,如下代码所示:
package com.openlab;
import sun.misc.Launcher;
public class ClassLoaderTest03 {
public static void main(String[] args){
String pathBoot = System.getProperty("sun.boot.class.path");
System.out.println(pathBoot.replaceAll(";",System.lineSeparator()));
System.out.println("---------------------------------");
String pathExt = System.getProperty("java.ext.dirs");
System.out.println(pathExt.replaceAll(";",System.lineSeparator()));
System.out.println("---------------------------------");
String pathApp = System.getProperty("java.class.path");
System.out.println(pathApp.replaceAll(";",System.lineSeparator()));
}
}
**getProperty()**这个方法是获取指定键指示的系统属性的,也就是说上面的代码获取的是sun.boot.class.path、java.ext.dirs 和 java.class.pat 的属性。
执行结果:
自定义加载器是在写一下类库的时候或者修改底层框架时,想加载哪个类就可以加载谁。
例如 Tomcat 加载的Servlet类,Spring框架中加载ApplicationContext类等等。
如下为ClassLoader类中的类加载过程的源码:
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 {
//继续使用parent的classloader 递归调用loadClass方法 向上
if (parent != null) { //如果此类加载器有父类加载器,那么先用父类类加载器加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);//如果父加载器为空,则默认用Bootstrap加载器加载
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 调用findClass方法去找class 向下
if (c == null) { //如果所有的父加载器包括Bootstrap都不能加载此类,此时c为null,那么就调用用户自定义的类加载器的findClass方法进行加载,
// If still not found, then invoke findClass in order所以用户自定义的类加载器要重写ClassLoader的findClass方法
// 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;
}
}
自定义加载器的实例:
首先先写一个自己定义的类:
package com.openlab.ClassLoader;
public class Myclass {
public Myclass() {
System.out.println("myclass");
}
}
在写一个自定义加载器:
package com.openlab.ClassLoader;
import java.io.*;
//自定义加载器
//1.继承ClassLoader
//2.重写findClass,在findClass里获取类的字节码,并调用ClassLoader中的defineClass方法来加载类,获取class对象
public class MyClassLoader extends ClassLoader{
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
File file = new File("E:\\ideacode\\JVM\\Myclass.class");
byte[] data = new byte[0];
try {
data = loadClassByte(file);
} catch (Exception e) {
e.printStackTrace();
}
return this.defineClass(name,data,0,data.length);
}
//将class文件转化为二进制字节码,通过IO流
private byte[] loadClassByte(File file) throws IOException {
//文件输入流
FileInputStream fin = new FileInputStream(file);
//此类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray() 和 toString() 获取数据。
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int len = 0;
while ((len = fin.read(data)) != -1) {
baos.write(data,0,len);
}
fin.close();
baos.close();
return baos.toByteArray();
}
}
这里就是我们的自定义类加载器了,我们重写了findClass方法,findClass方法中,调用了我们的loadClassByte方法,将class文件转为byte数组返回。
进行测试,运用反射的知识:
public class Text01 {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class> clazz = Class.forName("com.openlab.ClassLoader.Myclass",true,classLoader);
Object o = clazz.newInstance();
System.out.println(o.getClass().getClassLoader());
}
}
但是一开始的结果依然是应用类加载器!!!原因很简单了,双亲模型帮我们把类给加载了。
至于原因是,那就是我们idea会将我们工作空间地址配置给应用类加载器,所以我们的代码都是应用类加载器完成的!
解决这个问题有两个办法:
打破双亲委派模式,不让父加载器去加载就行了~~~
把我们工作空间的类剪切到桌面去,父加载器加载不到就只能自定义类加载器去加载了!!!
最终测试执行结果:
我们可以定义自己格式的classloader,一般情况下class文件就是一个二进制文件流,可以采用一种比较简单的方式对class文件进行加密和解密。
package com.openlab;
import java.io.*;
public class MacluClassLoaderWithEncription extends ClassLoader{
public static int seed = 0B10110110; // 进行参加加密算法的数字
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
File file = new File(
"c:/test",
name.replaceAll(".","/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b = fis.read())!=0){
baos.write(b^seed);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
return defineClass(name,bytes,0,bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);// throw ClassNotFoundException
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, IOException {
encFile("com.openlab.Person");
ClassLoader cl = new MacluClassLoaderWithEncription();
Class clazz = cl.loadClass("com.openlab.Person");
Person person = (Person) clazz.newInstance();
person.m();
System.out.println(cl.getClass().getClassLoader());
System.out.println(cl.getParent());
}
private static void encFile(String name) throws IOException {
File file = new File(
"c:/test/",
name.replace(".","/").concat(".class"));
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(
new File("c:/test",name.replaceAll(".","/").concat(".macluclass")));
int b = 0;
while ((b = fis.read())!=-1){
fos.write(b^seed);
}
fis.close();
fos.close();
}
}
生成加密好的文件
验证加密文件 打开后是乱码。
如上图所示,字节码解释器被称为 bytecode-interpreter,JIT即时编辑器被称为 Just In Time compiler。
Java语言既是一个解释型语言也是一个编译式的语言;想用解释的可以用解释器,想用编译也可以用编译器,具体看需求是怎么写的,可以通过JVM的一些参数进行设置。默认的情况是一种混合模式。
混合模式:使用解释器 + 热点编辑器 hotspot;起始阶段采用解释来执行,热点代码的检测默认值为10000:
当这样的一个循环或者是一个方法,或者是一段代码,一直都会被多次调用的时候,也就是这段代码执行频率特别高的情况下,那么干脆直接将这段代码编译成本地的代码,在下次直接访问的时候,直接访问本地的代码就可以,就不需要解释器对其进行解释执行,从而达到效率的提升。这种执行代码的方式被称为混合模式。
那么为什么不直接编译成本地代码,编译的执行速度更快?能够提高效率?
现在的解释器的执行效率已经是非常高的了,在一些简单的代码执行上,它并不属于编译器。
如果要执行的程序,依赖的类库特别多的情况下,在虚拟机中编译一遍,那么启动的过程会非常的缓慢。
通过JVM的参数进行设置:
-Xmixed 为混合模式:
开始解释执行,启动速度比较快,对热点代码进行检测和编译。
-Xint 解释模式:
启动速度很快,执行较慢
-Xcomp 纯编译模式:
启动较慢,执行较快
测试这三个jvm参数:
package com.openlab;
public class WayToRunTest01 {
public static void main(String[] args){
//这段代码被短时间执行很多次,请JVM虚拟机对其进行优化
for (int i = 0;i<10_0000;i++)
m();
long start = System.currentTimeMillis();
for (int i = 0;i<10_0000;i++){
m();
}
long end = System.currentTimeMillis();
System.out.println(end-start);
}
// 该方法本身没有意义,就是耗时间用的。
public static void m(){
for (int i = 0;i<10_0000L;i++){
long j = i%3;
}
}
}
默认的 -Xmixed 混合模式执行结果:
在JVM的执行参数中 -Xint 解释模式执行结果:
纯编译的模式-Xcomp 执行结果:
1.懒加载严格来讲应该叫 lazyInitializing;
2.在JVM规范中并没有规定什么时候加载;
3.JVM 严格规定了什么时候必须初始化:
以下这个案例,主要看什么时候打印P和X:
package com.openlab;
public class LazyLoadingTest {
public static void main(String[] args) throws ClassNotFoundException {
P p; //1
X x = new X();//2
System.out.println(P.i);//3
System.out.println(P.j);//4
Class.forName("com.openlab.LazyLoadingTest$P");//5
}
public static class P{
final static int i=8;// 打印final的值是不需要加载整个类的
static int j = 9;
static{
System.out.println("P");
}
}
public static class X extends P{
static{
System.out.println("X");
}
}
}
测试:
当只有1那一行的代码时,执行 1 那一行的时候什么也没打印,执行结果:
当只有2那一行的代码时,执行 2 那一行的时候,执行结果如下:
当只有3那一行的代码时,执行 3 那一行的时候,执行结果如下:
当只有4那一行的代码时,执行 4 那一行的时候,执行结果如下:
当只有5那一行的代码时,执行 5 那一行的时候,执行结果如下:
atic int i=8;// 打印final的值是不需要加载整个类的
static int j = 9;
static{
System.out.println(“P”);
}
}
public static class X extends P{
static{
System.out.println("X");
}
}
}
测试:
当只有1那一行的代码时,执行 1 那一行的时候什么也没打印,执行结果:
[外链图片转存中...(img-kLwJC7Dl-1647612890101)]
当只有2那一行的代码时,执行 2 那一行的时候,执行结果如下:
[外链图片转存中...(img-afKgkbnB-1647612890101)]
当只有3那一行的代码时,执行 3 那一行的时候,执行结果如下:
[外链图片转存中...(img-PjpVQOZO-1647612890102)]
当只有4那一行的代码时,执行 4 那一行的时候,执行结果如下:
[外链图片转存中...(img-HPAWzXhB-1647612890102)]
当只有5那一行的代码时,执行 5 那一行的时候,执行结果如下:
[外链图片转存中...(img-pZsDnZDX-1647612890103)]