JVM概述与类的加载机制
JVM 内存模型
对象逃逸分析、JVM 内存分配和回收策略
垃圾回收算法详解、垃圾收集器全解
JVM 调优
JVM概述与类的加载机制
1 概览
1.1 jdk 体系结构
java 虚拟机阵营:Sun HotSpot VM, BEA JRockit VM, IBM, J9 VM, Azul VM, Apache Harmony, Googole Dalvik VM, Microsoft JVM,,,,,,
jvm 再向底层结构图如下:
1.2 java 虚拟机
JVM(Java Virtual Machine,java 虚拟机):指以软件的方式模拟具有完整硬件系统功能,运行在一个完全隔离环境中的完整计算机系统,是物理机的软件实现。常用的虚拟机有 VMWare,Virtual Box,Java Virtual Machine
jvm 内存结构图
2 类加载机制
2.1 类加载过程
类加载:类加载器将 class 文件加载到虚拟机的内存。
类加载流程图如下:
-
类加载:类加载器将 class 文件加载到虚拟机的内存。
-
加载:在硬盘上查找并通过 IO 读入字节码文件。
-
连接:执行校验,准备,解析步骤。
-
校验:校验字节码的正确性(-Xverifynone 设置该参数关闭大部分的验证)。
-
准备:给类的静态变量分配内存,并赋予默认值。
-
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main() )替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的符和引用替换为直接引用。
-
初始化:对类的静态变量初始化为指定的值,执行静态代码块。
我们重点来看下“解析”。
我们创建一个 Math 类,代码如下:
public class Math {
public static final Integer CONSTANT = 666;
public static User user = null;
// 一个方法对应一块栈帧内存区域
public int compute(){
int a =1;
int b =2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
// User user = new User();
}
}
我们运行 main 方法后,进入 Math.class 所在文件夹,进入 Terminal 终端,对 Math.class 进行翻汇编,并把结果保存为 Math.txt 文件(javap -v Math.class > Math.txt),如下图:
接下来我们打开这个 Math.txt 文件,内容如下:
上面红色方框内,#3 就是符号引用,它用到了#2 和#35,解析这步骤要做的就是把间接引用替换为直接引用,例如,把 Methodred 的引用替换为#2 和#35 引用的目标。这整个过程是在类加载期间完成的,这就是静态链接。而动态链接是在程序运行期间完成的。
2.2 类加载器
2.2.1 java 中有如下几种类加载器
-
启动类加载器:负责加载支撑 JVM 运行的位于 JRE 的 lib 目录下的核心类库,比如,rt.jar,charsets.jar 等。
-
扩展类加载器:负责加载支撑 JVM 运行位于 JRE 的 lib 目录的 ext 扩展目录中的 JAR 类包。
-
应用程序类加载器:负责加载 ClassPath 路径下的类包,主要就是加载你自己写的那些类。
-
自定义加载器:负责加载用户自定义路径下的类包。
我们来用代码测试下看看:
/**
* 测试类加载器
*/
public class TestJDKClassLoadr {
public static void main(String[] args) {
// 底层用 C 语言实现的,所以找不到
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoadr.class.getClassLoader().getClass().getName());
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}
输出如下:
2.2.2 自定义类加载器
我们自己来写一个类加载器 MyClassLoader,加载我们的类 User1,User1.java 代码如下:
package com.example.demo.vo;
public class User1 {
public void sout(){
System.out.println("=================自己的类加载器加载类调用方法=====================");
}
}
MyClassLoader.java 代码如下:
/**
* 自定义类加载器
* 准备把类 User1{@link com.example.demo.vo.User1}的编译后文件 User1.class 文件复制一份放到 C:/temp/com/example/demo/vo 目录下
*/
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 读入文件保存成字节数
* @param name
* @return
* @throws Exception
*/
private byte[] loadByte(String name) throws Exception{
name = name.replaceAll("\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
/**
* 重写父类的 findClass 方法
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 从 classPath + name 即,C:/temp/com/example/demo/vo 下找到 User1.class
* 加载到包 com.example.demo.vo 中 即,com.example.demo.vo.User1
* 如果 target 下的包 com.example.demo.vo 里面有 User1.class 了,那么就会用类加载器
* ApplicationClassLoader 去加载 User1.class 了
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("c:/temp");
Class clazz = classLoader.loadClass("com.example.demo.vo.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行后,输出如下:
2.2.3 双亲委派机制
2.2.3.1 介绍
双亲委派机制
加载某个类时会先委托父加载器去加载,找不到再委托上层父加载器加载,如果所有的父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
类加载的双亲委派机制如下图:
上图的类加载器加载顺序是什么呢?
在实际应用中,我们很少自己写类加载器。假如,现在有一个.class 文件要加载。在没有自定义类加载器的情况下。
应用程序类加载器先不加载,它向上委托给其父加载器让扩展类加载器加载;
到了扩展类加载器那儿,它也先不加载,再向上委托给其父加载器让启动类加载器去加载;
到了启动类加载器那儿,它如果可以加载就把.class 文件加载到 jvm 中,加载完成。如果它加载不了,就又返回给子加载器扩展类加载器加载;
如果返回到扩展类加载器这儿,扩展类加载器可以加载了,加载完成。如果它加载不了,就又返回给子加载器应用程序类加载器去加载。
如果存在自定义类加载器,原理也是一样的。
我们来看个例子:
编写一个 String 类,和原有的 String 类在同一个包名,如下:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("==================My String Class ===============");
}
}
我们运行该类,看能不能打印出我们编写字符串“==================My String Class ===============”。控制台打印如下:
可以看到她说 java.lang.String 类中找不到 main 方法,可是我们明明写了啊。其实是这样的,
当运行时,加载 String.class 时,根据双亲委派机制,一直向上委托,启动类加载器发现 rt.jar 中有 String.class 文件,就加载了。所以运行时用到的 String.class 就不是我们编写的,用到是 jdk 中原有的 String.class 文件了,这个文件中没有 main 方法,所以就会报找不到 main 方法了。
2.2.3.2 原理
为什么要设计双亲委派机制?
沙箱安全机制:自己写的 String.class 类不会被加载,这样可以防止核心 API 库被随意篡改。
避免类的重复加载:当父加载器已经加载了该类时,就没有必要子加载器再加载一次,保证被加载类的唯一性。
我们以应用类加载器 AppClassLoader 展开分析:
找到 AppClassLoader 的源码,即 AppClassLoader.class,找到 loadClass 方法,如下:
我们点进去这个 super.loadClass(var1, var2),如下图:
接着看 c=findClass(name);自己去加载这块。我们再 AppClassLoader 中寻找 findClass 方法,发现没有找到,那么我们去其父类 URLClassLoader 中去寻找,源码如下: