目录
类的生命周期(生成类对象)
步骤3的准备,此时静态的引用成员变量为null,非引用成员变量为默认值(详细参考文章)
步骤4的解析,此时如public static final
修饰的作为常量的变量会被真正的常量所替代
前面5个步骤合起来就是传统所说的类加载的整个过程
其中2、3、4合起来统称为链接阶段
类加载并且初始化
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main 方法所在的类
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是 new 一个类的时候要初始化
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类
- 子类的初始化会触发父类的初始化
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化
- 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化
- 当初次调用 MethodHandle(方法指针) 实例时,初始化该 MethodHandle 指向的方法所在的类
package jvm;
import java.util.function.Function;
public class HelloByteCode {
public static void main(String[] args) {
HelloByteCode helloByteCode = new HelloByteCode();
System.out.println(MethodHandle(new HelloByteCode()::int2Str));
}
//方法指针function
private static String MethodHandle(Function<Integer,String> function){
String itoStrVal = function.apply(1);
return itoStrVal;
}
public String int2Str(int i){
return String.valueOf(i);
}
}
类可能会加载但不会初始化
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类初始化
- 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始化
- 通过 Class.forName 加载指定的类并生成类对象时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化,
Class.forName(jvm.Hello)
默认会加载 Hello 类,生成Hello类对象 - 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)
三类加载器
三类加载器:
- 启动类加载器(BootstrapClassLoader)
用于加载JVM最核心的一些jar包和类class文件,这些类都在JDk安装文件夹下的,几乎是所有Java程序都需要用到的一些公共类、最核心类(比如rt.jar)
- 扩展类加载器(ExtClassLoader)
作为启动类的补充加载器,负责给JVM加载一些额外的jar包或类class文件,这些jar包或类class文件存放在JDK或者JRE指定的地方(扩展类文件夹或目录),比如web应用项目的lab目录下的jar包也是通过扩展类加载器加载
- 应用类加载器(AppClassLoader)
用于加载我们自己编写的JVM代码生成的类class文件和它们依赖的一些其它类class文件、我们运行时指定的class path 下的jar包和类class文件
- 自定义类加载器
继承了应用类加载器,用于使用自定义的规则去加载类class文件或jar包
处于被启动类加载器和扩展类加载器加载的目录范围内的jar包或类都会在JVM启动时默认加载
加载器特点
- 双亲委托
当需要加载类class文件或者jar包时,首先去委托父类加载器进行类的加载,作用是防止重复加载,如果父类加载器已经加载过时,会直接把子类加载器需要加载的类class文件或jar包加载后的引用传递给子类加载器
- 负责依赖
当类加载器加载类class文件或者jar包时,发现这些类class文件和jar包还依赖了其它的类class文件或jar包,那么此时会一起被加载
- 缓存加载
已经加载过的类class文件或jar包,会被缓存到内存中,下次再遇到需要加载已经加载过的类class文件或jar包时,就不需要重复去加载,直接去内存里面取
- 单例的类对象
类对象是单例的,因为同一个类可以被不同的ClassLoader对象重复加载并生成类对象,但是不同ClassLoader之间加载的类空间(classpath)不互通且同一个类空间不存在相同的完全相同的两个类
充要类
ClassLoder是JDK里面的充要类,有一个具体的实现类就是URLClassLoder,通过指定URL的方式去加载类或者jar包,其它的类加载都是URLClassLoder的子类
启动类加载器(BootstrapClassLoader)是无法看到具体实现和代码的,因为它是由JVM底层实现的
除了启动类加载器,其它类加载器及其父类加载器都是可以通过ClassLoder类加载器拿到
获取类加载器及其class path
package jvm;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
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());
}
private static void printClassloader(String name, ClassLoader classLoader) {
System.out.println();
if (null != classLoader) {
System.out.println(name + " Classloader -> " + classLoader.toString());
printURLForClassloader(classLoader);
} else {
System.out.println(name + " Classloader -> null");
}
}
private static void printURLForClassloader(ClassLoader classLoader) {
//每一个ClassLoder都包含ucp属性
Object ucp = insightField(classLoader,"ucp");
//ucp属性中包含path属性,包含了ucp所属类加载器的所有class path
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 ex) {
ex.printStackTrace();
}
return null;
}
}
输出结果:
启动类加载器
===> file:/D:/java_oprationsystem/jre/lib/resources.jar
===> file:/D:/java_oprationsystem/jre/lib/rt.jar
===> file:/D:/java_oprationsystem/jre/lib/sunrsasign.jar
===> file:/D:/java_oprationsystem/jre/lib/jsse.jar
===> file:/D:/java_oprationsystem/jre/lib/jce.jar
===> file:/D:/java_oprationsystem/jre/lib/charsets.jar
===> file:/D:/java_oprationsystem/jre/lib/jfr.jar
===> file:/D:/java_oprationsystem/jre/classes
扩展类加载器 Classloader -> sun.misc.Launcher$ExtClassLoader@7a92922
===> file:/D:/java_oprationsystem/jre/lib/ext/access-bridge-64.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/cldrdata.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/dnsns.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/jaccess.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/jfxrt.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/localedata.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/nashorn.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunec.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunjce_provider.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunmscapi.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunpkcs11.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/zipfs.jar
应用类加载器 Classloader -> sun.misc.Launcher$AppClassLoader@18b4aac2
===> file:/D:/java_oprationsystem/jre/lib/charsets.jar
===> file:/D:/java_oprationsystem/jre/lib/deploy.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/access-bridge-64.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/cldrdata.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/dnsns.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/jaccess.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/jfxrt.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/localedata.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/nashorn.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunec.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunjce_provider.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunmscapi.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/sunpkcs11.jar
===> file:/D:/java_oprationsystem/jre/lib/ext/zipfs.jar
===> file:/D:/java_oprationsystem/jre/lib/javaws.jar
===> file:/D:/java_oprationsystem/jre/lib/jce.jar
===> file:/D:/java_oprationsystem/jre/lib/jfr.jar
===> file:/D:/java_oprationsystem/jre/lib/jfxswt.jar
===> file:/D:/java_oprationsystem/jre/lib/jsse.jar
===> file:/D:/java_oprationsystem/jre/lib/management-agent.jar
===> file:/D:/java_oprationsystem/jre/lib/plugin.jar
===> file:/D:/java_oprationsystem/jre/lib/resources.jar
===> file:/D:/java_oprationsystem/jre/lib/rt.jar
===> file:/D:/MyJavaProject/online-study/study/out/production/study/
===> file:/D:/ideaIU-2018.3.6.win/lib/idea_rt.jar
===> file:/D:/ideaIU-2018.3.6.win/lib/rt/debugger-agent.jar
自定义ClassLoder
作用:
- 类的动态加载
- 对代码的加密保护
- 虽然类对象是单例的,但是同一个类可以被不同的ClassLoader对象重复加载并生成类对象,这是因为不同ClassLoader对象之间存放的被加载的类的空间不互通
通过字节码文件的反编译可以得到Java源代码,所以对字节码文件进行加密,再通过自定义的类加载器加载加密后的字节码文件(实际就是将加密的字节码文件先解密转换回正常的字节码文件二进制内容然后传到类加载器正常加载),在不影响代码正常运行的情况下,可以实现对代码的保护和加密
package jvm;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
/*自定义一个 Classloader,加载一个 Hello.xlass 文件,执行 hello 方法,此文件内容是一个 Hello.class 文件所有字节(x=255-x 模拟加密)处理后的文件*/
public class XlassLoader extends ClassLoader {
public static void main(String[] args) throws Exception {
// 相关参数
final String className = "Hello";
final String methodName = "hello";
// 创建类加载器
ClassLoader classLoader = new XlassLoader();
// 加载相应的类
Class<?> clazz = classLoader.loadClass(className);
// 看看里面有些什么方法
for (Method m : clazz.getDeclaredMethods()) {
System.out.println(clazz.getSimpleName() + "." + m.getName());
}
// 创建对象
Object instance = clazz.getDeclaredConstructor().newInstance();
// 调用实例方法
Method method = clazz.getMethod(methodName);
method.invoke(instance);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 如果支持包名, 则需要进行路径转换
String resourcePath = name.replace(".", "/");
// 文件后缀
final String suffix = ".xlass";
// 获取输入流
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resourcePath + suffix);
try {
// 读取数据
int length = inputStream.available();
byte[] byteArray = new byte[length];
inputStream.read(byteArray);
// 转换
byte[] classBytes = decode(byteArray);
// 通知底层定义这个类
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} finally {
close(inputStream);
}
}
// 解码
private static byte[] decode(byte[] byteArray) {
byte[] targetArray = new byte[byteArray.length];
for (int i = 0; i < byteArray.length; i++) {
//反向解密
targetArray[i] = (byte) (255 - byteArray[i]);
}
return targetArray;
}
// 关闭
private static void close(Closeable res) {
if (null != res) {
try {
res.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
添加引用类的几种方式( 添加额外的类class文件或jar包给类加载加载)
1、放到 JDK 的 lib/ext 下,或者在编译Java文件,启动Java进程时添加额外的参数 -Djava.ext.dirs
指定额外的路径添加到class path。
在这两种方式所涉及的目录中类class文件或jar包都会被添加进扩展类加载器的加载范围
2、 java-cp或-classpath
(将额外的类class文件或jar包所在路径添加到扩展类加载器的class path) 或者把额外的类class文件或jar包放到当前的class path
3、自定义 ClassLoader 加载
4、拿到当前执行类的 ClassLoader,反射调用 addUrl 方法添加 Jar 或路径(JDK9 无效)
package jvm;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class TestAddUrl {
public static void main(String[] args) throws Exception {
URLClassLoader classLoader = (URLClassLoader) TestAddUrl.class.getClassLoader();
String dir = "/Users/anywhere/Hello";
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
URL url = new URL(dir);
method.invoke(classLoader, url);
Class klass = Class.forName("Hello",true, classLoader);
//JDK9以后应用类加载器和扩展类加载器和URLClassLoder平级了,不再有继承关系,此时不能再通过当前类的类加载器拿到URLClassLoder,可以使用如下方法
//Class klass2 = Class.forName("Hello",true, new URLClassLoader(new URL[]{new URL(dir)}));
Object obj = klass.newInstance();
Method hello = klass.getDeclaredMethod("hello");
hello.invoke(obj);
}
}