java内存分析
JVM探究
- 什么是JVM:运行在操作系统之上,一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
简单来说JVM是用来解析和运行Java程序的;Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
2.JVM自身物理结构图:
简述类的加载过程
Java中的字节码(后续补充字节码相关内容):英文名为 bytecode , 是Java代码编译后的中间代码格式。JVM需要读取并解析字节码才能执行相应的任务。
Java字节码是JVM的指令集。JVM加载字节码格式的class文件,校验之后通过JIT编译器转换为本地机为代码执行。
运行java程序的第一步就是加载class文件/或输入流里面包含的字节码。我们用"类加载 (Class Loading)"来表 示:将class/interface名称映射为Class对象的一整个过程。这个过程还可以划分为更 具体的阶段: 加载,链接和初始化(loading, linking and initializing)。
类的生命周期
其中前五个部分(加载,验证,准备,解析,初始化)统称为类加载:
-
加载:
- 加载阶段也可以称为**“装载”**阶段。这个阶段主要的操作是:根据明确知道的class完全限定名,来获取二进制classfile格式的字节流,简单点说就是找到文件系统中/jar包中/或存在于任何地方的"class文件"。如果找不到二进制表示形式,则会抛出NoClassDefFound错误。装载阶段并不会检查 classfile 的语法和格式。类加载的整个过程主要由JVM和Java 的类加载系统共同完成,当然具体到loading阶段则是由JVM与具体的某一个类加载器(java.lang.classLoader)协作完成的。
-
校验:
- 链接过程的第一个阶段是 校验,确保class文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。
- 校验过程检查classfile的语义,判断常量池中的符号,并执行类型检查,主要目的是判断字节码的合法性,比如magic number(字节码中会提到),对版本号进行验证。这些检查过程中可能会抛出VerifyError,ClassFormatError或UnsupportedClassVersionError。
- 因为classfile的验证属是链接阶段的一部分,所以这个过程中可能需要加载其他类,在某个类的加载过程中,JVM必须加载其所有的超类和接口。如果类层次结构有问题(例如,该类是自己的超类或接口,死循环了),则JVM将抛出ClassCircularityError。而如果实现的接口并不是一个interface,或者声明的超类是一个interface也会抛出IncompatibleClassChangeError。
-
准备:
- 然后进入准备阶段,这个阶段将会创建静态字段,并将其初始化为标准默认值(比如null或者0值),并分配方法表,即在方法区中分配这些变量所使用的内存空间。请注意,准备阶段并未执行任何Java代码
- 例如:public static int i = 1;在准备阶段 i 的值会被初始化为0,后面在类初始化阶段才会执行赋值为1,但是下面如果使用final作为静态常量,某些JVM的行为就不一样了:public static final int i = 1;对应常量i,在准备阶段就会被赋值1。
-
解析:
- 然后进入可选的解析符号引用阶段,也就是解析常量池,主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析。简单的来说就是我们编写的代码中,当一个变量引用某个对象的时候,这个引用在.class文件中是以符号引用来存储的(相当于做了一个索引记录)。在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。加载一个class时, 需要加载所有的super类和super接口。
-
初始化:
- JVM规范明确规定,必须在类的"首次主动使用"时才能执行类初始化。
- 初始化的过程包括执行:类构造器方法、static静态变量赋值语句、static静态代码块
- 如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化。所以其实在java中初始化一个类,那么必然先初始化过java.lang.Object类,因为所有的java类都继承自java.lang.Object。
- ps: 为了提高性能,HotSpot JVM通常要等到类初始化时才去装载和链接类。因此,如果A类引用了B类,那么加载A类并不一定会去加载B类(除非需要进行验证)。主动对B类执行第一条指令时才会导致B类的初始化,这就需要先完成对B类的装载和链接
类的加载时机
了解了类的加载过程,我们再看看类的初始化何时会被触发呢?JVM规范枚举了下述多种触发情况:
显示触发:
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类;
- 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类,就是new一个类的时候要初始化;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
隐式触发:
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射API对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
同时以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化,Hello.class不会让Hello类初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName(“jvm.Hello”)默认会加载Hello类。
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是不初始化)
诸如 Class.forName(), ClassLoader.loadClass() 等Java API, 反射API, 以及 JNI_FindClass 都可以启动类加载。JVM本身也会进行类加载。比如在JVM启动时加载核心类,java.lang.Object, java.lang.Thread 等等。
类加载器机制
类加载过程可以描述为"通过一个类的全限定名a.b.c.XXClass来获取描述此类的Class 对象",这个过程由"类加载器(ClassLoader)"来完成。这样的好处在于,子类加载器可以复用父加载器加载的类。系统自带的类加载器分为三种:
- 启动类加载器(BootstrapClassLoader): 它用来加载Java的核心类,是用原生C++代码来实现的,并不继承自java.lang.ClassLoader(负责加载JDK中jre/lib/rt.jar里所有的class)。它可以看做是JVM自带的,我们再代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它,如果打印出来就是个null。举例来说,java.lang.String是由启动类加载器加载的,所以String.class.getClassLoader()就会返回null。但是后面可以看到可以通过命令行参数影响它加载什么。
- 扩展类加载器(ExtClassLoader): 它负责加载JRE的扩展目录,lib/ext或者启动java程序时在java启动命令中添加参数: -Djava.ext.dirs额外的添加指定JDK使用的类或jar包的扩展路径,代码里直接获取它的父类加载器为null(因为无法拿到启动类加载器。ps:这里的’父类加载器’并不是java类之间的继承关系,而是’双亲委托’机制中的层级关系,扩展类加载器和应用类加载器都继承自URLClassLoader类,而扩展类加载器的父类加载器是BootstrapClassLoader,应用类加载器的父类加载器是扩展类加载器);多个应用程序共性依赖的jar就可以放在扩展类加载器中,扩展类加载器是对启动类加载器和应用类加载器的一个补充。
- 应用类加载器(AppClassLoader): 它负责在JVM启动时加载来自Java命令的java -classpath或者- cp选项、java.class.path系统属性指定的jar包和类路径。在应用程序代码里可以通过ClassLoader的静态方法getSystemClassLoader()来获取应用类加载器。如果没有特别指定,即在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。
一般启动类加载器是由JVM内部实现的,在Java的API里无法拿到,但是我们可以侧面看到和影响它(后面的内容会演示)。后2种类加载器在Oracle Hotspot JVM里,都是在中sun.misc.Launcher定义的,扩展类加载器和应用类加载器一般都继承自URLClassLoader类,这个类也默认实现了从各种不同来源加载class字节码转换成Class的方法。
加载器类的继承关系:
此外还可以自定义类加载器。如果用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器,但是直接从ExClassLoader里拿不到它的引用,同样会返回null。
双亲委托层级关系:
类加载机制有三个特点:
- 双亲委托:当一个自定义类加载器需要加载一个类,比如java.lang.String,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载器如果发现自己还有父加载器,会一直往前找,这样只要上级加载器,比如启动类加载器已经加载了某个类比如java.lang.String,所有的子加载器都不需要自己加载了如果几个类加载器都没有加载到指定名称的类,那么会抛出ClassNotFountException异常。
- 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
- 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。
添加加载路径的几种方式,总结
1、放到JDK的lib/ext下,或者启动时指定-Djava.ext.dirs参数
2、java -cp/classpath或者class文件放到当前路径
3、自定义ClassLoader加载
4、拿到当前类的ClassLoader,反射调用addUrl方法添加Jar或路径(JDK9无效)
需要特别注意的是:JDK9以后,应用类加载器,扩展类加载器,跟URL类加载器是平级关系了,没有继承关系了,所以就没办法把应用类加载器和扩展类加载器转换成URL类加载器;在JDK9之后,我们可以直接在class.forNmae后面的参数上加一个new URLClassLoader,把我们需要添加的jar包或者文件夹作为URLClassLoader的参数指定进来
实例1:打印三种加载器加载的所有路径:
package com.river.classloader;
import sun.misc.Launcher;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
/**
* 测试类加载器:
*/
public class PrintClassLoaderTest {
public static void main(String[] args) {
/*
查看启动类加载器加载的路径:
启动类加载器:启动类加载器虽然我们在代码的层面上没办法直接拿到,
但是像Open JDK或者Oracle JDK,我们可以通过一个静态方法去拿到它的ClassPath,
从而拿到它内部所有的加载的路径
*/
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(" =====> " + url.toExternalForm());
}
// 扩展类加载器
ClassLoader extClassLoader = PrintClassLoaderTest.class.getClassLoader().getParent();
printClassLoader("扩展类加载器", extClassLoader);
// 应用类加载器:方式1
ClassLoader appClassLoader = PrintClassLoaderTest.class.getClassLoader();
// 应用类加载器:方式2
// ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); // 也可以通过这个方式获得应用类加载器
printClassLoader("应用类加载器", appClassLoader);
// 查看应用类加载器加载路径 方式2:
System.out.println("应用类加载器: " + System.getProperty("java.class.path"));
}
/**
* 打印输出classLoader的名字和其加载的所有路径
* @param name
* @param classLoader
*/
private static void printClassLoader(String name, ClassLoader classLoader) {
System.out.println();
if (null != classLoader) {
System.out.println(name + " ClassLoader -> " + classLoader.toString());
printClassLoaderUrl(classLoader);
} else {
System.out.println(name + " ClassLoder -> null");
}
}
/**
* 打印classLoader加载的所有路径
* @param classLoader
*/
private static void printClassLoaderUrl(ClassLoader classLoader) {
Object ucp = insightField(classLoader, "ucp");// 获取对应classLoader中的"ucp"字段,"ucp"的字段类型为:URLClassPath
Object path = insightField(ucp, "path");// 获取URLClassPath中隐藏了所有加载路径的列表
List paths = (List) path;
for (Object p : paths) {
System.out.println("======> " + p.toString());
}
}
/**
* 通过fName,获取obj中对应的字段
* @param obj
* @param fName
* @return
*/
private static Object insightField(Object obj, String fName) {
Field f = null;
try {
if(obj instanceof URLClassLoader) {
/*
1.getDeclaredFiled 获取类本身的属性成员(包括私有、共有、保护)
2.getField 获取类(及其父类可以自己测试) public属性成员
因为扩展类加载器和应用类加载器都继承自URLClassLoader,
所以可以把他们当作URLClassLoader,获取URLClassLoader中的'URLClassPath ucp'字段。
需要特别注意的是:JDK9以后,应用类加载器,扩展类加载器,跟URL类加载器是平级关系了,
没有继承关系了,所以就没办法把应用类加载器和扩展类加载器转换成URL类加载器
*/
f = URLClassLoader.class.getDeclaredField(fName);
} else {
f = obj.getClass().getDeclaredField(fName);
}
f.setAccessible(true); // 设置允许外部访问私有属性
return f.get(obj); // 方法:get(Object obj) 返回指定对象obj上此 Field表示的字段的值
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
实例2: 自定义类加载器
package com.river.classloader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* 自定义类加载器
* 可以用来做类的动态加载,class加密
*/
public class CustomizeClassLoader extends ClassLoader {
public CustomizeClassLoader() {
}
public static void main(String[] args) throws Exception {
Class helloClass = (new CustomizeClassLoader()).findClass("Hello"); // 装载class
Object o = helloClass.newInstance(); // 初始化
Method hello = helloClass.getMethod("hello");
hello.invoke(o);
}
// 重写加载方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将一个.class文件用BASE64加密成加密串
String helloBase64 = "NQFFQf///8v/4/X/+f/x9v/w/+/3/+71/+3/7Pj/6/j/6v7/+cOWkZaLwf7//NfWqf7/+7yQm5r+//CzlpGasYqSnZqNq56dk5r+//qXmpOTkP7/9ayQio2cmrmWk5r+//W3mpOTkNGVnome8//4//f4/+nz/+j/5/7/7Leak5OQ09+ck56MjLOQnpuajd74/+bz/+X/5P7/+reak5OQ/v/vlZ6JntCTnpGY0LCdlZqci/7/75WeiZ7Qk56RmNCshoyLmpL+//yQiov+/+qzlZ6JntCWkNCvjZaRi6yLjZqeksT+/+yVnome0JaQ0K+NlpGLrIuNmp6S/v/4j42WkYuTkf7/6tezlZ6JntCTnpGY0KyLjZaRmMTWqf/e//r/+f///f/+//j/9//+//bi//7//vrVSP/+Tv7/9fn//v7//v/0//f//v/22v/9//72Tf/97fxJ//tO/v/19f/9+//3//r//v/z/f/y";
// 读取后进行解密
byte[] bytes = this.decode(helloBase64);
for(int i = 0; i < bytes.length; ++i) {
byte temp = (byte)(~bytes[i]);
bytes[i] = temp;
}
Object var8 = null;
try {
new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException var7) {
var7.printStackTrace();
}
return this.defineClass(name, bytes, 0, bytes.length); // 装载class
}
public byte[] decode(String base64) {
return Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
}
}