概述:
- 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 {
public static final int a = 123;
public static int b = 222;
public static String str = "sdfsdf";
public int abc;
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 = "静态常量";
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("构造器"); }
public static void main(String[] args) { new Test02();
} }
|
验证初始化:
- 当main方法中new对象时,执行空:

此时会执行静态代码块中的语句,同时静态常量、静态变量都得到了初始化,所以main方法执行后,会初始化Test02类中的静态常量、静态变量、静态代码块
- 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); }
public static void main(String[] args) { new ChildClass(); } }
|
测试验证:
main方法中不执行任何语句:

当在子类的main中执行时,会先初始化父类的静态变量+静态代码块,然后再初始化子类的静态变量+静态代码块
main方法中new子类对象时:

前面的依旧,先初始化父类子类的静态变量、静态代码块,然后由于new了子类,所以还会先执行父类的成员变量、代码块、构造器,再执行子类的成员变量、代码块、构造器。
如果在子类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)上所有的类库;
|
测试验证:
比如在

- 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;
public class MyClassLoader extends ClassLoader {
@Override public Class<?> findClass(String name) throws ClassNotFoundException { try { 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;
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 {
Class cls = clazzCache.get(name); if (force) { cls = null; } String className = MyClassLoader.class.getPackage().getName() + "." + name;
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’,那么轮询时就会发现文件的修改
