java内存分析
简述类的加载过程
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。
双亲委托层级关系:
添加加载路径的几种方式,总结
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) {
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(" =====> " + url.toExternalForm());
}
ClassLoader extClassLoader = PrintClassLoaderTest.class.getClassLoader().getParent();
printClassLoader("扩展类加载器", extClassLoader);
ClassLoader appClassLoader = PrintClassLoaderTest.class.getClassLoader();
printClassLoader("应用类加载器", appClassLoader);
System.out.println("应用类加载器: " + System.getProperty("java.class.path"));
}
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");
}
}
private static void printClassLoaderUrl(ClassLoader classLoader) {
Object ucp = insightField(classLoader, "ucp");
Object path = insightField(ucp, "path");
List paths = (List) path;
for (Object p : paths) {
System.out.println("======> " + p.toString());
}
}
private static Object insightField(Object obj, String fName) {
Field f = null;
try {
if(obj instanceof URLClassLoader) {
f = URLClassLoader.class.getDeclaredField(fName);
} else {
f = obj.getClass().getDeclaredField(fName);
}
f.setAccessible(true);
return f.get(obj);
} 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;
public class CustomizeClassLoader extends ClassLoader {
public CustomizeClassLoader() {
}
public static void main(String[] args) throws Exception {
Class helloClass = (new CustomizeClassLoader()).findClass("Hello");
Object o = helloClass.newInstance();
Method hello = helloClass.getMethod("hello");
hello.invoke(o);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
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);
}
public byte[] decode(String base64) {
return Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
}
}
|