概述:

  • JVM类加载机制
  • 深入剖析JVM内存管理
  • JVM垃圾回收机制
  • JVM故障诊断性能调优

一、JVM类加载机制

1.1 Java运行时一个类是什么时候被加载的?

一个类在什么时候开始被加载,《Java虚拟机规范》中并没有进行强制约束,交给了虚拟机自己去自由实现,HotSpot虚拟机是按需加载,在需要用到该类的时候加载这个类;

1、Sun公司最早的 Classic虚拟机;

2、Sun/Oracle公司的HotSpot虚拟机;

3、BEA公司的JRockit虚拟机;

4、IBM公司的IBM J9虚拟机;

官方:https://docs.oracle.com/javase/8/

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+TraceClassLoading

测试类的详细载入:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2 JVM一个类的加载过程?

一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段:

1、加载(Loading)

2、验证(Verification)

3、准备(Preparation)

4、解析(Resolution)

5、初始化(Initialization)

6、使用(Using)

7、卸载(Unloading)

其中验证、准备、解析三个阶段统称为连接(Linking);
在这里插入图片描述
加载: classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间,此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载;

验证: 验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全;

准备: 类变量赋默认初始值,int为0,long为0L,boolean为false,引用类型为null;常量赋正式值;

解析: 把符号引用翻译为直接引用;

初始化: 当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化…… 那么这些都会触发类的初始化;

使用: 使用这个类;

卸载:

1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;

2.加载该类的ClassLoader已经被GC; (ClassLoader:类加载器,用于加载class)

3.该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;

准备阶段:

​ 对于final常量来说,准备阶段直接赋值a为123,对于类变量,是在类初始化(new)的时候才会进行赋值,所以先赋0,而对于实例变量abc来说,也是先赋0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test01 { // java.lang.Class

//常量
public static final int a = 123;

//类变量
public static int b = 222;//0

public static String str = "sdfsdf"; //null

//实例变量
public int abc;

// -XX:+TraceClassLoading 监控类的加载
public static void main(String[] args) {
User user = new User();
user.working();
System.out.println(b);

Order order = new Order();
}
}

解析阶段:

把一个类的class文件读进来之后,变成java.lang.class对象,在元空间里面

1.3一个类被初始化的过程?☆☆☆

在这里插入图片描述
类的初始化阶段,Java虚拟机才真正开始执行类中编写Java程序代码

进行准备阶段时,变量已经赋过一次系统要求的初始零值,

而在初始化阶段,才真正初始化类变量和其他资源;

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
public class Test02 {

// 静态常量 ==准备
public static final String staticConstantField = "静态常量";

// 静态变量 ==准备阶段赋值为 null,初始化阶段赋值为 静态变量
public static String staticField = "静态变量";

// 变量 == 创建对象的时候赋值
public String field = "变量";

// 静态初始化块 ==初始化阶段执行
static {
System.out.println(staticConstantField);
System.out.println(staticField);
System.out.println("静态初始化块");
}

// 初始化块 == 创建对象的时候执行
{
System.out.println(field);
System.out.println("初始化块");
}

// 构造器 == 创建对象的时候执行
public Test02() {
System.out.println("构造器");
}

// java Test02
public static void main(String[] args) {
new Test02();

// 1、rt.jar charset.jar
// 2、InitialOrderTest
}
}

验证初始化:

  1. 当main方法中new对象时,执行空:
    在这里插入图片描述
    此时会执行静态代码块中的语句,同时静态常量、静态变量都得到了初始化,所以main方法执行后,会初始化Test02类中的静态常量、静态变量、静态代码块
  1. main中new Test02对象时:
    在这里插入图片描述
    同样也会先初始化Test02中的静态常量、静态变量、静态代码块,和第一个验证一致,其次再去初始化类的成员变量、代码块、构造方法

1.4 继承时父子类的初始化顺序是怎样的?

父类–静态变量

父类–静态初始化块

子类–静态变量

子类–静态初始化块

父类–变量

父类–初始化块

父类–构造器

子类–变量

子类–初始化块

子类–构造器

父类:

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
public class ParentClass {

// 静态变量
public static String p_StaticField = "父类--静态变量";

// 变量
public String p_Field = "父类--变量";

protected int i = 0;
protected int j = 0;

// 静态初始化块
static {
System.out.println(p_StaticField);
System.out.println("父类--静态初始化块");
}

// 初始化块
{
System.out.println(p_Field);
System.out.println("父类--初始化块");
}

// 构造器
public ParentClass() {
System.out.print("父类--构造器");
System.out.println("i=" + i + ", j=" + j);
i = 1; j = 1;
}
}

子类:

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
public class ChildClass extends ParentClass {

// 静态变量
public static String s_StaticField = "子类--静态变量";

// 变量
public String s_Field = "子类--变量";

// 静态初始化块
static {
System.out.println(s_StaticField);
System.out.println("子类--静态初始化块");
}

// 初始化块
{
System.out.println(s_Field);
System.out.println("子类--初始化块");
}

// 构造器
public ChildClass() {
System.out.print("子类--构造器");
System.out.println("i=" + i + ",j=" + j);
}

// 程序入口 java ChildClass
public static void main(String[] args) {
new ChildClass();
}
}

测试验证:

  1. main方法中不执行任何语句:
    在这里插入图片描述
    当在子类的main中执行时,会先初始化父类的静态变量+静态代码块,然后再初始化子类的静态变量+静态代码块

  2. main方法中new子类对象时:
    在这里插入图片描述
    前面的依旧,先初始化父类子类的静态变量、静态代码块,然后由于new了子类,所以还会先执行父类的成员变量、代码块、构造器,再执行子类的成员变量、代码块、构造器。

  3. 如果在子类main中new父类对象
    在这里插入图片描述
    结果可想而知,由于是子类的main,会先初始化父类子类静态变量、静态代码块,然后由于new了父类,所以还会初始化父类的成员变量、代码块、构造器

1.5 究竟什么是类加载器?

在这里插入图片描述
在类“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的“代码”被称为“类加载器”(Class Loader),这个 动作是可以自定义实现 的;

类加载器可以由C++、Java语言实现

简单来说,就是把.class文件以二进制形式读进来

1.6 JVM有哪些类加载器?

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

1、启动类加载器(Bootstrap ClassLoader),使用C++语言实现,是虚拟机自身的一部分;

2、其他所有的类加载器,由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;

站在Java开发者的角度来看,自JDK 1.2开始,Java一直保持着三层类加载器架构;
在这里插入图片描述

1.7 JVM中不同的类加载器加载哪些文件?

1、启动类加载器(Bootstrap ClassLoader):(根的类加载器)C++语言实现的,在java中通过getClassLoader方法是获取不到的

1
2
3
<JAVA_HOME>\jre\lib\rt.jar,resources.jar、charsets.jar

被-Xbootclasspath参数所指定的路径中存放的类库;

2、扩展类加载器(Extension ClassLoader):

1
2
3
4
5
6

sun.misc.Launcher$ExtClassLoader,

<JAVA_HOME>\jre\lib\ext,

被java.ext.dirs系统变量所指定的路径中所有的类库;

3、应用程序类加载器(Application ClassLoader):系统的类加载器

1
2
3
sun.misc.Launcher$AppClassLoader

加载用户类路径(ClassPath)上所有的类库;

测试验证:

比如在

  • \jre\lib\rt.jar下找到一个类BufferReader类,查看其classLoader,为空,由于是c++语言编写的,所以Java中获取不到,是正常的;

    输出为null则表明根的类加载器

在这里插入图片描述

  • AppClassLoader,包括自己写的代码以及项目中涉及到的第三方jar包(如Springboot)
    在这里插入图片描述
    继承结构:
    在这里插入图片描述

    1.8 JVM三层类加载器之间的关系是继承吗?

    在这里插入图片描述
    不是继承关系。

解释:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
换个角度思考这个问题,前面提到过BootstrapClassLoader是C++语言实现的,ExtClassLoader是Java实现的,怎么可能继承呢?

如果我们自定义一个类加载器,那么则是继承共同的父类ClassLoader(抽象类)

1.9 JVM类加载的双亲委派模型 ☆☆☆

在这里插入图片描述
官方描述:

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,下一层类加载器才会尝试自己去加载;

过程分析:

如:我要加载String这个类,来自rt.jar,首先让App ClassLoader去加载,但它自己不加载,委派给上一层加载,ExtClassLoader也不去加载,再继续委派给Bootstrap ClassLoader,再往上就没有了,那么此时BootstrapClassLoader就要去尝试加载,它主要是加载jre下内部库的jar包,而String这个类正好在rt.jar中,所以就return了,从而把String这个类加载到JVM内存中了。

当BootstrapClassLoader加载内库后,找不到所要加载的类,那么就让第二层ExtClassLoader去尝试加载,找得到就return,找不到就继续让AppClassLoader去加载后找。

当这个类几个类加载器都找不到时,报ClassNotFoundException

解释一下为啥叫双亲:因为AppClassLoader上面有两层,所以叫双亲

总结:先自底向上委派,再自顶向下去尝试加载

1.10 JDK为什么要设计双亲委派模型,有什么好处?

1、确保安全,避免Java核心类库被修改;

2、避免重复加载;

3、保证类的唯一性;

如果你写一个jaa.lang.String的类去运行,发现会抛出如下异常;

解释:

比如即使自己写了个String类,也不会被加载,因为最先会加载rt.jar下的String,能够避免核心内库被修改,要保证安全。
在这里插入图片描述
在这里插入图片描述
自定义的包理论上是最终被APPClassLoader去加载的,但是这里为啥会报错,是因为包和核心内库重名了,所以报安全异常

1.11 可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?

可以;

想要打破这种模型,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可;

1.12 如何自定义自己的类加载器?

1
2
1、继承ClassLoader
2、覆盖findClass(String name)方法 或者 loadClass() 方法;

findClass(String name)方法 不会打破双亲委派;

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
import java.io.IOException;
import java.io.InputStream;

/**
* 自定义ClassLoader
*
*/
public class MyClassLoader extends ClassLoader {

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
//public Class<?> loadClass(String name) throws ClassNotFoundException {
//name = 包名 + 类名
try {
// User.class
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
throw new ClassNotFoundException(name);
}
byte[] b = new byte[is.available()];
is.read(b);

return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test04 {

public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();

Class clazz = myClassLoader.findClass("com.bjpowernode.loader.User");
System.out.println(clazz.getClassLoader());
Object instance = clazz.newInstance();
System.out.println(instance);

System.out.println(User.class.getClassLoader());
Class claz2 = User.class;
Object o2 = claz2.newInstance();
System.out.println(o2);
}
}

本来com.bjpowernode.loader.User应该去AppClassLoader中加载的,但由于自定义了myClassLoader,并调用了findClass方法,所以会使用自定义的方法去加载。

在这里插入图片描述

1.13 ClassLoader中的loadClass()、findClass()、defineClass()区别?

loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;

findClass() 根据名称或位置加载.class字节码;

definclass() 把字节码转化为java.lang.Class;

1、当我们想要自定义一个类加载器的时候,并且想破坏双亲委派模型时,我们会重写loadClass()方法;

2、如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?可以可以重写findClass方法(),findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现;

JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中;

因为覆盖即打破双亲委派,所以建议改findClass

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass()中实现你自己的加载逻辑即可;

经典案例:Tomcat就打破了双亲委派机制

1.14 加载一个类采用Class.forName()和ClassLoader有什么区别

在这里插入图片描述

1
2
3
4
5
6
7
8
9
public class Test05 {

public static void main(String[] args) throws IOException {
Class.forName("com.jvm.demo.loader.ChildClass")

Class clazz = Test05.class.getClassLoader().loadClass("com.jvm.demo.loader.ChildClass")
}

}

在这里插入图片描述
在这里插入图片描述

分析区别:
由此可见,ClassLoader这种方式,只经过了加载->链接,但是没有初始化,但是forName方式会初始化
如果继续对类实例化,则会初始化,并会创建对象

1
2
3
4
5
6
7
8
9
public class Test05 {

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

Class clazz = Test05.class.getClassLoader().loadClass("com.jvm.demo.loader.ChildClass");
clazz.newInstance();
}

}

在这里插入图片描述
看看两个方法底层实现:

forName底层会初始化类:

调用了一个本地C++方法
在这里插入图片描述
ClassLoader底层:

双亲委派类加载,没有对类进行初始化
在这里插入图片描述

1.15 了解Tomcat 的类加载机制

在这里插入图片描述
可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和每个Web应用的类加载器+JSP类加载器;

3个基础类加载器在 conf/catalina.properties 中进行配置:

common.loader=”${catalina.base}/lib”,”${catalina.base}/lib/.jar”,”${catalina.home}/lib”,”${catalina.home}/lib/.jar”
server.loader=
shared.loader=

Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派的机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类,我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,我们可以看到WebAppClassLoader类中重写了loadClass方法;

1.16 有没有听说过热加载和热部署,如何自己实现一个热加载?

热加载 是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于Java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境;

热部署 是指可以在不重启服务的情况下重新部署整个项目,比如Tomcat热部署就是在程序运行时,如果我们修改了War包中的内容,那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包生成新的文件夹;

1、热加载是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变;

2、热部署是在运行时重新部署整个项目,耗时相对较高;

如何实现热加载呢?

在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重新进行加载;

重点步骤

1、实现自己的类加载器;

2、从自己的类加载器中加载要热加载的类;

3、不断轮训要热加载的类class文件是否有更新,如果有更新,重新加载;

类加载器

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
72
73
74
75
76
77
78
79
80
81
82
83
84
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* 自定义一个类加载器
*
* bootstrap ClassLoader jdk/jre/目录下的jar包加载
*
* ext ClassLoader jdk/ext/目录下的jar包加载
*
* App ClassLoader --我们应用的ClassLoader
*
*/
public class MyClassLoader extends ClassLoader {

private File classPathFile;

private static Map<String, Class> clazzCache = new ConcurrentHashMap<>();//缓存 保存加载的类

public MyClassLoader() {
String classPath = MyClassLoader.class.getResource("").getPath();
this.classPathFile = new File(classPath);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return this.findClass(name, false);
}

protected Class<?> findClass(String name, boolean force) throws ClassNotFoundException {
//force是否强制加载

//之前有没有加载过这个class
Class cls = clazzCache.get(name);
if (force) {
//如果强制加载 则缓存值值为空,后面重新加载一遍
cls = null;
}
String className = MyClassLoader.class.getPackage().getName() + "." + name;

//如果这个cls不为空则直接return了
if (cls == null && classPathFile != null) {
File classFile = new File(classPathFile + "\\" + name.replaceAll("\\.", "/") + ".class");
if (classFile.exists()) {
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
fis = new FileInputStream(classFile);
byte[] bytes = new byte[4096];
bos = new ByteArrayOutputStream();
int len;
while ((len = fis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
cls = defineClass(className, bos.toByteArray(), 0, bos.size());
clazzCache.put(name, cls);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

return clazzCache.get(name);
}
}

写一个定时任务去轮询:

观察狗 线程:

思路:遍历文件,去拿到每个文件的时间,判断是否被修改,如果修改则重新触发加载类(自定义类加载器)

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
import java.io.File;
import java.util.Map;

/**
* 观察狗
*
*/
public class WatchDog implements Runnable {

private Map<String, FileDefine> fileDefineMap;

public WatchDog(Map<String, FileDefine> fileDefineMap) {
this.fileDefineMap = fileDefineMap;
}

@Override
public void run() {
File file = new File(this.getClass().getResource("").getPath());
File[] files = file.listFiles();
for (File watchFile : files) {
long newTime = watchFile.lastModified();
FileDefine fileDefine = fileDefineMap.get(watchFile.getPath());
long oldTime = fileDefine.getLastDefine();
//如果文件被修改了,那么重新生成累加载器加载新文件
if (newTime != oldTime) {
System.out.println("文件被修改......");
fileDefine.setLastDefine(newTime);
//重新触发类加载
loadMyClass(watchFile.getName());
}
}
}

public void loadMyClass(String className) {
try {
MyClassLoader myClassLoader = new MyClassLoader();
myClassLoader.findClass(className.replace(".class", ""), true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
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
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* 测试运行
*/
public class Test {

public static void main(String[] args) throws Exception {
Map<String, FileDefine> fileDefineMap = new ConcurrentHashMap<>();

File file = new File(Test.class.getResource("").getPath());
File[] files = file.listFiles();
for (File watchFile : files) {
FileDefine fileDefine = new FileDefine();
fileDefine.setLastDefine(watchFile.lastModified());
fileDefineMap.put(watchFile.getPath(), fileDefine);
}

//定时任务
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(new WatchDog(fileDefineMap), 3, 3, TimeUnit.SECONDS);

MyClassLoader w = new MyClassLoader();
while (true) {
Class clazz = w.findClass("MyLog");
try {
ILog myLog = (ILog) clazz.newInstance();
myLog.log();
Thread.sleep(2000);
}catch (Exception e) {
e.printStackTrace();
}
}
}
}

测试其中一个修改类:

1
2
3
4
5
6
7
public class MyLog implements ILog {

@Override
public void log() {
System.out.println("log, version 1.0"); 、
}
}

此处修改 System.out.println(“log, version 1.0”);为 System.out.println(“log, version 2.0”); 文件发生修改,但是要编译一下,点击build->Recompile ‘’MyLog.java’,那么轮询时就会发现文件的修改

在这里插入图片描述