(一)JVM入门以及Class文件架构、类加载器

JVM入门以及Class文件架构、类加载器 加载过程

Posted by Steven on 2021-05-11
Estimated Reading Time 23 Minutes
Words 5.6k In Total
Viewed Times

一、引言

首先看一张图

image-20210511172041345

Java 是编译型语言还是解释型语言?

有人说Java是编译型的。因为所有的Java代码都是要编译的,.java不经过编译就无法执行。

也有人说Java是解释型的。因为java代码编译后不能直接运行,它是解释运行在JVM上的,所以它是解释型的。

可以说Java是兼具编译型语言与解释型语言的特点的。

另外,一些常用的代码,其实会被JIT即时编译器进行编译

Java对于多种不同的操作系统有不同的JVM,所以实现了真正意义上的跨平台。

并且现在能在JVM上跑的语言已经有一百多种了

JVM与class文件格式

也就是说任何语言,只要最终是class文件,符合JVM class文件的规范,就可以跑在JVM上

JVM跟Java无关

image-20210511174020255

因此,jvm是一种规范 ,在这里可以看到各个版本的JVM的规范是什么,比如Jvms13

jvm是虚拟出来的一台计算机,他有自己的CPU,字节码指令集(汇编语言),有自己的内存等

常见的JVM实现

  • hotspot: oracle官方,我们现在用的就是这个

    1
    2
    3
    4
    5
    Steven@StevendeMacBook-Pro ~ % java -version 
    java version "1.8.0_92"
    Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
    Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)
    Steven@StevendeMacBook-Pro ~ %
  • Jrockit

  • J9 :IBM

  • Microsoft VM

  • TaobaoVM :hotspot深度定制版

  • LiquidVm:直接针对硬件

  • azul zing:最新垃圾回收的业界标杆

二、class文件

class文件还是有些复杂的,如何学习呢,先知道她的主要结构,然后一般使用IDEA插件jclasslib,然后从一个空的类,加属性,加变量,加方法,等等,分别通过插件查看生成的结构来学习

class文件的格式是一个二进制的字节流,这种文件当我们使用16进制的编译器(如subline)打开,看到的就是这样的

image-20210511233054160

当然也可以使用8进制、10进制、2进制打开

而这些二进制的字节流就是由Java虚拟机来解释的,那是如何解释的呢,可以通过看class文件的组成理解

具体可以参考这篇文章 Class文件结构

三、class文件结构

1、魔数:

每个class文件的头4个字节称为魔数(Magic Number),唯一作用是用于确定这个文件是不是一个Class文件(一个能被虚拟机接受的class文件)。Class文件魔数的值为0xCAFEBABE(16进制的CAFEBABE)。如果一个文件不是以0xCAFEBABE开头,那就肯定不是Class文件

2、class文件的版本号

次版本号(minor_version): 前2字节用于表示次版本号
主版本号(major_version): 后2字节用于表示主版本号。

这个的版本号是随着jdk版本的不同而表示不同的版本范围的。Java的版本号是从45开始的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。

0X0034(对应十进制的50):JDK1.8
0X0033(对应十进制的50):JDK1.7
0X0032(对应十进制的50):JDK1.6
0X0031(对应十进制的49):JDK1.5  
0X0030(对应十进制的48):JDK1.4  
0X002F(对应十进制的47):JDK1.3  
0X002E(对应十进制的46):JDK1.2
ps:0X表示16进制

3、常量池:

长度为constant_pool_count-1的表
image-20210512165232458

可以查看这个结构的方法主要有

  1. Java自带的javap,比如
    image-20210512165808468
  2. IDEA插件jclasslib
    image-20210512170040590
    image-20210512170923374
  3. image-20210512170939057
  4. JBE:可以直接修改

常量池是比较复杂的,具体可以参考这篇文章 Class文件结构

3、访问标志(2字节)(access_flags)

这个标志主要用于识别一些类或接口层次的访问信息

4、类索引、父类索引和接口索引集合

这三项数据主要用于确定这个类的继承关系

5、字段表集合(fields)

6、方法表集合(method)

7、属性表集合(attribute)


加载过程之Loading

四、详解Class加载过程

class文件是怎么从硬盘上到内存中,并且开始准备执行的

三步:

  • Loading
    讲class文件load到内存
  • Linking:
    • Verfication: 校验,校验是否符合class文件的标准
    • Preparation:将class文件中的静态变量赋默认值
    • Resolution:把class文件中常亮池用到的符号引用。转化成内存地址,可以直接使用
  • Initlalizing:静态变量赋值为初始值

image-20210514152056010

首先如何加载到内存中呢?

JVM有一个类加载器的层次,这个类加载器本身就是一个普通的class,类加载器的层次加载不同的class,jvm中所有的class都是被类加载器加载到内存中的,这个类加载器就叫做ClassLoader

image-20210513103710126

一个class从硬盘中被加载到内存中之后,实际上创建了两块内容,第一块是把class文件放到内存中,另一块内容是生成了一个class类的对象,这个对象指向了第一块内容

  • 第一个类加载器层次
    BootStrap:加载Lib里最核心的内容,比如rt.jar,charset.jar等核心,所以说当我调用getClassLoader()得到的加载器的结果是一个空值的时候,代表这个加载器已经到达了最顶层的类加载器
  • 第二个类加载器的层次
    Extension:加载扩展包中的文件,这些扩展包在jdk安装目录的ext
  • 第三个类加载器的层次
    APP:这个就是平时用的类加载器,用于加载classpath指定的内容
  • 第四个类加载器的层次
    自定义加载器,加载自定义的加载器

CustomerClassLoader的父类加载器是>Application,他的父类加载器是> Extension,他的父类加载器是BootStrap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.shifpeng.scaffold.service.jvm;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class ClassLoaderLevel {
public static void main(String[] args) {
//查看是哪个加载器load到内存中的,该执行结果为null,是因为Bootstrap加载器是由C++实现的,Java里面没有class和他对应
System.out.println(String.class.getClassLoader());
//也是被Bootstrap加载器加载的,所以也是null
System.out.println(sun.awt.HKSCS.class.getClassLoader());
//这个类位于ext目录下某个jar文件里面,所以类加载器就是ExtClassLoader
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
//我们自己写的类,是由AppClassLoader加载的
System.out.println(ClassLoaderLevel.class.getClassLoader());

//它是Ext的classloader调用调用他的getclass,但是ext加载器本身也是一个class,所以这个class的getClassLoader就是最顶级的Bootstrap类加载器,所以结果为null
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
System.out.println(ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader());


}
}

⚠️注意:父加载器不是“类加载器的加载器”!!!也不是“类加载器的父类加载器”,而上面的例子是指如果找不到对应的类加载器,委托给父加载器找

五、双亲委派

任何一个class,假如自定义了classloader,这个时候会尝试在CustomerClassLoader中找(类加载器中维护者缓存),如果已经加载进来了,那直接返回结果,如果没有,并不是立马把这个类加载进来 ,而是委托父级ApplicationCLassloader,看看它有没有加载这个类,如果有直接返回,如果没有,再委托给Ext,Ext有就返回,没有就再委托Bootstrap,有就返回

Bootstrap如果没有,就**返回 ** 委托Ext加载器加载这个类,如果这个加载器可以加载这个类,那么就直接加载并返回(按照上面的四层加载器分别可以加载的类别),如果不能加载该类,再向下委托给App,如果还是不能加载,继续向下委托给自定义类加载器CustomerClassLoader,如果加载成功,即返回,如果不成功,则会抛出ClassNotFound的异常

这个过程就叫做** 双亲委派**

image-20210513115012992

为什么要搞双亲委派机制 ?

主要是为了安全(防止篡改),次要的就是为了防止重复加载,资源浪费;

如果任何的class都可以把class加载进内容的话,假如我把Oracle写的内部核心的java.lang.String,我就可以自己写并且覆盖他,交给自定义的ClassLoader加载,把这个Stringload进内存,打包给客户,然后把密码存储成string类型的对象,我就可以偷偷的把密码发给我自己,那就不安全了

而双亲委派就会产生警觉,先去看看父级有没有加载过,有加载过就直接返回,不会重复加载

双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程

如果从源码的角度理解父加载器,就是有一个class的对象,里面有一个成员变量叫parent,这个parent是ClassLoader类型的,这个parent指定为哪个类加载器就是哪个加载器,我们所说的父加载器就是这个parent对象

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.shifpeng.scaffold.service.jvm;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class ClassLoaderParentAndChild {
public static void main(String[] args) {
System.out.println(ClassLoaderParentAndChild.class.getClassLoader()); //AppClassLoader
System.out.println(ClassLoaderParentAndChild.class.getClassLoader().getClass().getClassLoader()); //NULL
System.out.println(ClassLoaderParentAndChild.class.getClassLoader().getParent()); //ExtClassLoader
System.out.println(ClassLoaderParentAndChild.class.getClassLoader().getParent().getParent()); //null
}
}

六、类加载器的范围

我们在打印类加载器的时候会显示

1
sun.misc.Launcher$AppClassLoader@7f31245a

Launcher是ClassLoader的一个包装类启动类

BootstrapExtensionApp这些的加载路径是如何指定的呢?就是来自于Launcher源码,在源码中可以看到这几个指定的路径,当然按照这些方式我们也可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.shifpeng.scaffold.service.jvm;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class ClassLoaderScope {
public static void main(String[] args) {
String pathBoot = System.getProperty("sun.boot.class.path");
//注意 MAC OS系统这里用: ,而win系统用;
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()));
}
}

image-20210513145139392

七、自定义类加载器(ClassLoader源码)

想要自定义类加载器就需要稍微读一读ClassLoader源码

首先如果要把一个类加载到内存的时候,应该怎么做?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.shifpeng.scaffold.service.jvm;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class LoadClassbyHand {
public static void main(String[] args) throws ClassNotFoundException {
Class class1 = LoadClassbyHand.class.getClassLoader().loadClass("com.shifpeng.scaffold.service.jvm.ClassLoaderScope");
System.out.println(class1.getName());
}
}
//返回:
//com.shifpeng.scaffold.service.jvm.ClassLoaderScope

这里有个loadClass方法

通过源码我们看到,只需要调用loadClass方法,把这个类加载到内存中,然后返回一个Class类的对象

也就是前面说的,从硬盘上找到这个类的源码,然后加载到内存中,与此同时生成一个Class的对象,返回这个对象

1
2
3
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

热部署就是需要一个ClassLoader将类手动load到内存中

ClassLoader就是反射的基石

我们再看下方法中的loadClass()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    // The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent; //这里的parent是final的,所以是改不了的

/**
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object 需不需要解析(把符号地址转化成内存地址)
*
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) { //加锁,防止load到一半,其他的也加载进来了
// First, check if the class has already been loaded
//检查是否已经load进来了
//这里具体的实现只能读JVM的源码了,hotspot源码了
Class<?> c = findLoadedClass(name);
if (c == null) { //如果没有加载进来
long t0 = System.nanoTime();
try {
if (parent != null) {
//递归方法进行委派父亲加载器进行load
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();
//
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;
}
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

在向上找是否有类加载器已经加载了该类,如果没找到,就开始向下找符合加载该类的加载器,这里调用了一个findClass()方法,这个方法是protected的,也就是说只能在子类中访问;但是我们发现这个方法的实现直接抛出了一个异常ClassNotFoundException,因此只能自定义ClassLoader重写这个findClass,因此其实每个父加载器都有重写这个findClass方法

因此,如果我们要自定义ClassLoader,我们只需要做一件事就可以,就是定义自己的findClass()

这里用到的是设计模式中的模板方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.shifpeng.scaffold.service.jvm;

import com.shifpeng.scaffold.service.jvm.dao.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/

/**
* 自定义的ClassLoader
*/
public class CustomizeClassLoader extends ClassLoader {

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("/Users/Steven/test/", name.replace(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;

while ((b = fis.read()) != 0) {
baos.write(b);
}

byte[] bytes = baos.toByteArray();
baos.close();
fis.close();//可以写的更加严谨

return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}

/**
* 使用
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
ClassLoader l = new CustomizeClassLoader();
//这样就可以load指定目录下的指定类了
//这里就会按照双亲委派机制进行load(上来先找,找不到去父亲里面找,一圈找不到,只能回来自己加载 )
Class clazz = l.loadClass("com.shifpeng.scaffold.service.jvm.dao.Hello");
Class clazz1 = l.loadClass("com.shifpeng.scaffold.service.jvm.dao.Hello");

System.out.println(clazz == clazz1);

Hello h = (Hello) clazz.newInstance();
h.m();

System.out.println(l.getClass().getClassLoader());
System.out.println(l.getParent());

System.out.println(getSystemClassLoader());
}

}
//输出如下

image-20210513170458451

其中,class文件如下

1
2
3
4
5
Steven@StevendeMacBook-Pro dao % ls
Hello.class
Steven@StevendeMacBook-Pro dao % pwd
/Users/Steven/test/com/shifpeng/scaffold/service/jvm/dao
Steven@StevendeMacBook-Pro dao %

说明类已经加载进来了

loadclass过程:findCCache->parent.loadClass->findClass

八、加密

通过自定义ClassLoader,可以给自己的代码加密,下面就是一种简单的加密方式

a异或两次就是它自己. b^seed^seed=b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.shifpeng.scaffold.service.jvm;

import com.shifpeng.scaffold.service.jvm.dao.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class ClassLoaderWithEncription extends ClassLoader {
public static int seed = 0B10110110;

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("/Users/Steven/test/", name.replace('.', '/').concat(".msbclass"));

try {
FileInputStream fis = new FileInputStream(f);
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 (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}

public static void main(String[] args) throws Exception {

encFile("com.shifpeng.scaffold.service.jvm.dao.Hello");

ClassLoader l = new ClassLoaderWithEncription();
Class clazz = l.loadClass("com.shifpeng.scaffold.service.jvm.dao.Hello");
Hello h = (Hello)clazz.newInstance();
h.m();

System.out.println(l.getClass().getClassLoader());
System.out.println(l.getParent());
}

private static void encFile(String name) throws Exception {
File f = new File("/Users/Steven/test/", name.replace('.', '/').concat(".class"));
FileInputStream fis = new FileInputStream(f);
FileOutputStream fos = new FileOutputStream(new File("/Users/Steven/test/", name.replaceAll(".", "/").concat(".msbclass")));
int b = 0;

while((b = fis.read()) != -1) {
//对读出的字节流进行异或操作
fos.write(b ^ seed);
}

fis.close();
fos.close();
}

}

九、JVM之混合模式

刚才一直说的是加载,这里要说一下编译

Java是解释执行的,一个class文件load到内存中,通过Java的解释器(interpreter)解释执行。

JIT

Java有一个叫做JIT(Just in-Time compiler)的东西,有些代码需要编译成为本地代码,相当于win中的exe可执行文件

默认模式

  • 混合使用解释器+热点代码编译

    热点代码编译:一段代码里面的一个循环或者一个方法在执行的时候刚开始是用解释器执行,结果发现在整个执行过程中,这段代码执行的频率非常高,我就会把这段代码编译成本地代码(如win的exe,linux的elf),将来再执行的时候执行我本地的这段代码,效率提升

    那么为什么不直接都编译成本地代码?那岂不是效率更高了
    1、因为java的解释器现在的效率其实已经很高了,在一些简单的代码的执行上不输于编译器

    2、如果有一段代码执行文件特别多,各种各样的类库有好几十个class很正常,如果上来直接用编译器编译,编译过程会非常长,所以默认模式是混合模式

    3、完全可以使用参数来制定到底用什么模式

    1
    2
    3
    -Xmixed 混合模式,开始解释执行,启动速度较快。对热点代码实行检测和编译
    -Xint 使用解释模式,启动很快,执行很慢
    -Xcomp 使用纯编译模式,执行很快,启动很慢(很多类的时候)
  • 起始阶段采用解释执行

  • 热点代码检测

    • 多次被调用的方法
      方法计数器:检测方法执行的频率
    • 多次被调用的循环
      循环计数器:检测循环被执行的频率
    • 进行编译
    1
    2
    //检测热点代码:
    -XX:CompileThreshold=10000

我们进行个小测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.shifpeng.scaffold.service.jvm;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class WayToRun {
public static void main(String[] args) {
for(int i=0; i<10000; i++)
m();

long start = System.currentTimeMillis();
for(int i=0; i<10000; i++) {
m();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}

public static void m() {
for(long i=0; i<10_0000L; i++) {
long j = i%3;
}
}

}

在Jvm的参数配置这里,先不填任何,即混合模式试试

image-20210514115923185

执行后结果为0.86s,我们设置为解释模式试试

image-20210514120248716

执行后时间为21.57s

纯编译执行时间为0.7s (如果类特别多的话,启动就会非常慢)

因此m()方法被检测到大量重复执行,所以会对这个方法进行编译,而其他的一些代码就是解释执行

十、 懒加载(lazyLoading)

严格的讲应该叫做lazyInitialzing懒初始化

  1. JVM规范并没有规定何时加载,什么时候需要这个类再去加载

  2. 但是严格规定了什么时候必须初始化(共五种情况,了解即可 )

    Java虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

    1.遇到new,getstatic,putstatic和invokestatic这4条指令时,如果类没有初始化时,则需要先触发其初始化。生成这4条指令的最常见Java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法。

    1. 使用java.lang.reflect包的方法对类进行反射调用时候。

    2. 当初始化一个类时,发现其父类没有进行初始化,则需先触发其父类的初始化。

    3. 当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),当前类需要先初始化

    4. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_putStatic、REF_getStatic、REF_invokeStatic的方法句柄,并且此方法句柄所对应的类没有进行初始化时,需要初始化该类。

而上面说到class文件从硬盘加载到内存中,主要过程是这样的Loading->Linking->Initlalizing

  • Loading
    讲class文件load到内存
  • Linking:
    • Verfication: 校验,校验是否符合class文件的标准
    • Preparation:将class文件中的静态变量赋默认值
    • Resolution:把class文件中常亮池用到的符号引用。转化成内存地址,可以直接使用
  • Initlalizing:静态变量赋值为初始值

我们说需要这个类的时候才会被加载,所以如何判断是否加载了呢,看下这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.shifpeng.scaffold.service.jvm;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class LazyLoading {
public static void main(String[] args) throws Exception {
// P p; //这一步执行的时候,也不会被加载,根据那5种情况
// X x = new X(); //肯定被加载出来了
// System.out.println(P.i); //没有被加载 但是值会被打印出来,打印final的值是不需要加载的
// System.out.println(P.j); //加载了(非Final)
Class.forName("com.shifpeng.scaffold.service.jvm.LazyLoading$P"); //P是LazyLoading的内部类 加载了

}

public static class P {
final static int i = 8;
static int j = 9;
static {
System.out.println("P");
}
}

public static class X extends P {
static {
System.out.println("X");
}
}

}

Initlalizing会执行上面``System.out.println(“P”);的静态语句块,而P`一旦被打印,说明这个类被load进来了


下节说加载过程之Linking加载过程之intializing

扩展:自定义ClassLoader的parent是怎么指定的?

任何一个classLoader,都有一个parent,,而我们自定义的ClassLoader并没有指定parent,那就需要去看源码

我们在new我们自定义的ClassLoader的时候,会调用父类的无参构造方法

1
2
3
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

这里的 getSystemClassLoader(),系统的ClassLoaderAppClassLoader,这个可以自己验证下就可以

方法里面又调用了自己的另外一个构造方法

1
2
3
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

这个方法中已经指定了parent,即上面传入的AppClassLoader

我们也是可以自己指定指定父亲的ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.shifpeng.scaffold.service.jvm;

/**
* @Project: scaffold
* @Description:
* @Author: Steven
**/
public class ParentClassLoader {
private static CustomizeClassLoader parent = new CustomizeClassLoader();

private static class MyLoader extends ClassLoader {
public MyLoader() {
super(parent);
}
}

}

用自己订单的ClassLoader继承ClassLoader,构造方法里面调用super指定parent


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !