代码经过编译变成了字节码打包成 Jar 文件。让 JVM 去加载需要的字节码,变成持久代/元数据区上的 Class 对象,接着执行程序逻辑。
步骤:加载->链接(校验->准备->解析)->初始化->使用->卸载
加载:根据明确知道的 class 完全限定名, 来获取二进制 classfile 格式的字节流(找到文件系统中/jar 包中/或存在于任何地方的“class 文件”。 如果找不到二进制表示形式,则会抛出 NoClassDefFound 错误。)
校验:确保 class 文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。
ClassCircularityError。 而如果实现的接口并不是一个 interface,或者声明的超类是一个 interface,也会抛出 IncompatibleClassChangeError。准备:会创建静态字段, 并将其初始化为标准默认值(比如null或者0 值),并分配方法表,即在方法区中分配这些变量所使用的内存空间。
i的值会被初始化为 0,后面在类初始化阶段才会执行赋值为 1;解析:进入可选的解析符号引用阶段。 也就是解析常量池,主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析。
.class 文件中是以符号引用来存储的(相当于做了一个索引记录)。初始化: 必须在类的首次“主动使用”时才能执行类初始化。
java.lang.Object 类,因为所有的 java 类都继承自 java.lang.Object。触发类的初始化情况:
同时以下几种情况不会执行类初始化:
示例: 诸如 Class.forName(), classLoader.loadClass() 等 Java API, 反射API, 以及 JNI_FindClass 都可以启动类加载。 JVM 本身也会进行类加载。 比如在 JVM 启动时加载核心类,java.lang.Object, java.lang.Thread 等等。
类加载过程可以描述为“通过一个类的全限定名 a.b.c.XXClass 来获取描述此类的 Class 对象”,这个过程由“类加载器(ClassLoader)”来完成。这样的好处在于,子类加载器可以复用父加载器加载的类。
sun.misc.Launcher定义的。一般都继承自URLClassLoader类,这个类也默认实现了从各种不同来源加载 class 字节码转换成 Class 的方法。sun.misc.Launcher定义的,一般都继承自URLClassLoader类,这个类也默认实现了从各种不同来源加载 class 字节码转换成 Class 的方法。public class Hello {
static {
System.out.println("Hello Class Initialized!");
}
}
import java.util.Base64;
public class HelloClassLoader extends ClassLoader {
public static void main(String[] args) {
try {
new HelloClassLoader().findClass("jvm.Hello").newInstance(); // 加载并初始化Hello类
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
String helloBase64 = "yv66vgAAADQAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2N" +
"hbFZhcmlhYmxlVGFibGUBAAR0aGlzAQALTGp2bS9IZWxsbzsBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAHAAgHABkMABoAGwEAGEhlb" +
"GxvIENsYXNzIEluaXRpYWxpemVkIQcAHAwAHQAeAQAJanZtL0hlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2" +
"YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAgABAAcACA" +
"ABAAkAAAAvAAEAAQAAAAUqtwABsQAAAAIACgAAAAYAAQAAAAMACwAAAAwAAQAAAAUADAANAAAACAAOAAgAAQAJAAAAJQACAAAAAAAJsgACEgO2AASxAAAAAQAK" +
"AAAACgACAAAABgAIAAcAAQAPAAAAAgAQ";
byte[] bytes = decode(helloBase64);
return defineClass(name,bytes,0,bytes.length);
}
public byte[] decode(String base64){
return Base64.getDecoder().decode(base64);
}
}
排查再找不到jar包的问题
结果可以看到三种类加载器各自默认加载了哪些 jar 包和包含了哪些 classpath 的路径
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
public class JvmClassLoaderPrintPath {
public static void main(String[] args) {
// 启动类加载器
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("启动类加载器");
for(URL url : urls) {
System.out.println(" ==> " +url.toExternalForm());
}
// 扩展类加载器
printClassLoader("扩展类加载器", JvmClassLoaderPrintPath.class.getClassLoader().getParent());
// 应用类加载器
printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());
}
public static void printClassLoader(String name, ClassLoader CL){
if(CL != null) {
System.out.println(name + " ClassLoader -> " + CL.toString());
printURLForClassLoader(CL);
}else{
System.out.println(name + " ClassLoader -> null");
}
}
public static void printURLForClassLoader(ClassLoader CL){
Object ucp = insightField(CL,"ucp");
Object path = insightField(ucp,"path");
ArrayList ps = (ArrayList) path;
for (Object p : ps){
System.out.println(" ==> " + p.toString());
}
}
private static Object insightField(Object obj, String fName) {
try {
Field f = null;
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;
}
}
}
如何排查类的方法不一致的问题
java.lang.NoSuchMethodError?怎么看到加载了哪些类,以及加载顺序?
-XX:+TraceClassLoading 或者 -verbose 即可,注意需要加载 Java 命令之后,要执行的类名之前,不然不起作用。例如:java -XX:+TraceClassLoading jvm.HelloClassLoader怎么调整或修改 ext 和本地加载路径?
从前面的例子我们可以看到,假如什么都不设置,直接执行 java 命令,默认也会加载非常多的 jar 包,怎么可以自定义加载哪些 jar 包呢?比如我的代码很简单,只加载 rt.jar 行不行?答案是肯定的。
$ java -Dsun.boot.class.path="D:\Program Files\Java\jre1.8.0_231\lib\rt.jar" -Djava.ext.dirs= jvm.JvmClassLoaderPrintPath
启动类加载器
==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/rt.jar
扩展类加载器 ClassLoader -> sun.misc.Launcher$ExtClassLoader@15db9742
应用类加载器 ClassLoader -> sun.misc.Launcher$AppClassLoader@73d16e93
==> file:/D:/git/studyjava/build/classes/java/main/
==> file:/D:/git/studyjava/build/resources/main
-Dsun.boot.class.path表示我们要指定启动类加载器加载什么,最基础的东西都在 rt.jar 这个包了里,所以一般配置它就够了。需要注意的是因为在 windows 系统默认 JDK 安装路径有个空格,所以需要把整个路径用双引号括起来,如果路径没有空格,或是 Linux/Mac 系统,就不需要双引号了。-Djava.ext.dirs表示扩展类加载器要加载什么,一般情况下不需要的话可以直接配置为空即可。怎么运行期加载额外的 jar 包或者 class 呢?
有时候在程序已经运行了以后,还想要再额外的去加载一些 jar 或类.简单说就是不使用命令行参数的情况下,怎么用代码来运行时改变加载类的路径和方式
假如说,在d:/app/jvm路径下,有刚才使用过的 Hello.class 文件,怎么在代码里能加载这个 Hello 类呢?
package jvm;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class JvmAppClassLoaderAddURL {
public static void main(String[] args) {
String appPath = "file:/d:/app/";
URLClassLoader urlClassLoader = (URLClassLoader) JvmAppClassLoaderAddURL.class.getClassLoader();
try {
Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addURL.setAccessible(true);
URL url = new URL(appPath);
addURL.invoke(urlClassLoader, url);
Class.forName("jvm.Hello"); // 效果跟Class.forName("jvm.Hello").newInstance()一样
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行以下,结果如下:
$ java JvmAppClassLoaderAddURL Hello Class Initialized!
结果显示 Hello 类被加载,成功的初始化并执行了其中的代码逻辑。