类加载和初始化
class cycle
首先一个class文件在硬盘里面
然后JVM去对它进行以下行为:
- Loading,把class文件load到内存,双亲委派(安全)
Linking,分三小步:
verification,校验文件是否复合JVM规定的class格式.<br /> preparation,静态变量赋默认值,比如int-0,double-0.0,boolean-false<br /> resolution,解析,将类、方法、属性等符号引用解析为直接引用<br /> 如常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
Initializing,初始化,这时候静态变量赋初始值(代码指定的值),调用静态代码
一般我们只要记住,静态变量的初始化分为两步,Linking时是默认值,Initializing后才是初始化的值
成员变量的初始化其实也是分两步,第一步申请内存空间时是默认值,第二步调用构造方法时才是初始化的值
类加载器ClassLoader
一个class文件的Loading,load出两个东西:
- class文件的二进制编码加载到内存
- 生成一个与之对应的Class对象,改对象指向内存中的class编码文件
如果打印一下String的ClassLoader.会发现结果为null:
System.out.println(String.class.getClassLoader());
这是因为:
最顶层的加载器Bootstrap是用C++来实现的,在JAVA中没有与之对应的类.
所以Bootstrap加载出来的类,比如String,获取到的ClassLoader是null.
类加载器的分层关系:
注意
1.这个上层加载器,即父加载器,是逻辑上的关系,其实就是一个成员变量
2.不是类的继承关系,那是另一种维度的关系
3.加载器也是一个对象,也要由另一个加载器加载,但并不一定是由他的parent加载,是谁不一定.最终都是由Bootstrap加载的
设一个加载器a的上层加载器是b,那么 a不一定是被b加载的
求证一下BootStrap,Ext,App都加载哪些类
其实AppClassLoader和ExtClassLoader都是sun.misc.Launcher的内部类
(JDK11把Launcher换成了ClassLoaders)
Launcher里面可以看到这三个内部类,和他们各自加载的path
BootClassPathHolder:System.getProperty(“sun.boot.class.path”)
AppClassLoader:java.class.path
ExtClassLoader:java.ext.dirs
我们写个方法打印一下这些东西就看出来了:
public static void main(String[] args) {
System.out.println("boot-------------------");
System.out.println(System.getProperty("sun.boot.class.path").replaceAll(":", System.lineSeparator()));
System.out.println("ext-------------------");
System.out.println(System.getProperty("java.ext.dirs").replaceAll(":", System.lineSeparator()));
System.out.println("classpath-------------------");
System.out.println(System.getProperty("java.class.path").replaceAll(":", System.lineSeparator()));
}
双亲委派
是什么
双亲委派并不是指父母双方,而是指”查找类时从子到父,加载类时从父到子”的这么一个机制.
具体含义:
众所周知,ClassLoader加载完一个类后,会放入一个ClassCache,下次再用时就不需重复加载了.每个ClassLoader有自己的ClassCache
- 当我们需要找一个类时,会先交给最下层的ClassLoader,在ClassCache找,如果找到了就返回结果,如果找不到就交给上层加载器,上层加载器进行同样的操作,直到Bootstrap.
- 真正去加载这个类的时候,会自上到下开始加载.
每个ClassLoader先看自己管辖的类里面有没有需要加载的class,如果有就加载返回,如果没有就交给下一层去加载.
如果都没有就抛异常CLassNotFoundException.
为什么
为啥不直接放到一个ClassCache里面,这样就不用层层查找了啊?
这里主要是出于安全考虑.
假设黑客小明自定义了一个java.lang.String对象,里面做了些非法操作;如果不分层查找的话,用户就会用到他自定义的String,阴谋得逞;
双亲委任机制下,使用String时,先看看父加载器是否已加载,直到找到Bootstrap后直接返回String类.
可以打破双亲委派机制吗?
可以,自定义一个classLoader,重写loadClass方法就可以打破.
热加载/热部署的时候,可能会重写loadClass(),打破双亲委派机制
从源码角度去理解ClassLoader
继承ClassLoader(这里用到了模板方法设计模式)
重写模板方法findClass,调用defineClass
自定义类加载器 加载 加密的class,防止反编译和篡改
ClassLoader简单读源码
别的方法ClassLoader类已经写好啦(模板设计模式),
关键点就是下面的findClass方法,该方法直接抛异常,是个必须被子类重写的方法.(模板方法,钩子函数)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 这个查找调用了native方法,具体可能要看HotSpot或者其他JVM源码了,可以理解为一个"缓存"
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
// 然后调用parent的loadClass,parent也是先检查下是否已加载
// 然后调用parent.parent.loadClass或者findBootstrapClassOrNull
// 这里体现了双亲委派的第一步,查找类时从子到父
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 这里需要各ClassLoader实现,需要去实现双亲委派的第二步,加载类时从父到子
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
查实自定义一个简单的Class Loader
如果Load的class本项目空间已经有啦,那么就不会走自定义的findClass方法了,而是直接由Launcher$AppClassLoader加载出来了
所以我们自定义一个CLassLoader,一般是加载一个其他地方的class,比如从RPC服务中获取
远程传输class文件一般会对对class加密,拿到class文件的字节数组后再解密;
最简单的是对方发送时对一个token做异或(xor,^)运算,我们拿到后再对那个token做异或即可解密.
我这里com.example.springboot2.test.Hello是另一个项目空间的类,test.Hi是自己的项目空间的类
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("MyClassLoader.findClass");
String parent = "/Users/liweizhi/IdeaProjects/wzdemo/springboot2/target/classes/";
// String parent = "/Users/liweizhi/IdeaProjects/wzdemo/demo1/target/classes";
File f = new File(parent, name.replace(".", "/").concat(".class"));
FileInputStream fis = null;
ByteArrayOutputStream bao = null;
try {
fis = new FileInputStream(f);
bao = new ByteArrayOutputStream();
/*int b = 0;
while ((b = fis.read()) != -1) {
bao.write(b);
}*/
byte[] buffer = new byte[1024];
for (int len = 0; (len = fis.read(buffer)) != -1; ) {
bao.write(buffer, 0, len);
}
byte[] bytes = bao.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bao != null) {
try {
bao.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.findClass(name);
}
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader = new MyClassLoader();
// String className = "com.example.springboot2.test.Hello";
String className = "test.Hi";
Class<?> aClass = classLoader.loadClass(className);
System.out.println(aClass.getClassLoader());
}
}
LazyLoading & 父子类的构造器加载顺序
Loading,把class文件load到内存;Initializing初始化
JVM规范并没有规定什么时候加载
但是规定了什么时候必须初始化:
- new getstatic putstatic invokestatic指令,访问final变量除外
- java.lang.reflect对类进行反射调用时
- 初始化子类的时候,父类首先初始化
- 虚拟机启动时,被执行的主类必须初始化
- 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
这里涉及到一个加载顺序的问题
加载顺序的问题:
- final static修饰的变量,在类加载前就初始化好了,访问它不需要初始化类.
- 访问静态变量时需要加载类,先加载父类执行父类的静态代码块,再加载自己执行自己的静态代码块.
如果有非静态代码块或者构造器的内的代码,整体的顺序是:
3.1 Parent static block<br /> 3.2Child static block(这时子类加载完毕,先加载父类)<br /> 3.3Patent block<br /> 3.4 Patent constructor<br /> 3.5 Child block<br /> 3.6 Child constructor(这时子类对象创建完毕,先调用父类的代码块和构造器)
```java public class InitializingTest { public static void main(String[] args) throws ClassNotFoundException { // System.out.println(Parent.a); // 先加载Parent,然后打印a // System.out.println(Parent.s); // System.out.println(Child.q); // 先加载Parent, 后加载Child, 然后打印q // System.out.println(Child.w); // Class.forName(“com.example.demo.jvm.InitializingTest$Parent”);
// 这个和打印Child.q时一样, 其实就是当第一次打印Child.q,去加载了
// Class.forName(“com.example.demo.jvm.InitializingTest$Child”);
Child child = new Child();
}
static class Parent {
static int a = 1;
static final int s = 2;
static {
System.out.println("Parent static block");
}
{
System.out.println("Patent block");
}
public Parent() {
System.out.println("Patent constructor");
}
}
static class Child extends Parent {
static int q = 3;
static final int w = 4;
static {
System.out.println("Child static block");
}
{
System.out.println("Child block");
}
public Child() {
System.out.println("Child constructor");
}
}
}
<a name="DwRvh"></a>
### java是解释型还是编译型语言?
答:默认是混合模式,解释器+JIT,当某个方法调用很频繁时就走JIT<br />也可以指定为单纯的解释性/编译型
1. 解释:众所周知,Java是跨平台的语言,JVM在运行时讲class字节码解释为操作系统认识的本地代码去执行
1. 编译:这里编译是指,直接编译成操作系统认识的本地代码,不用JVM在运行时解释了
1. 解释器: bytecode interpreter
1. JIT: Just In-Time compiler
1. 混合模式:混合使用解释器 + 热点代码编译器
5.1 其实阶段是解释执行<br /> 5.2 当一个方法或者循环指令的调用频率很高时,这部分代码由JIT进行编译,下次执行就不用解释了,提高速度
6. 可以通过JVM参指定模式:
-Xmixed 混合模式,默认<br /> -Xint 解释模式,启动快,执行慢<br /> -Xcomp 编译模式,启动慢,执行快<br />搞个测试类,求证一下:
```java
public class T009_WayToRun {
static int count = 10_0000;
public static void main(String[] args) {
for (int i = 0; i < count; i++) {
m();
}
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
m();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
public static void m() {
for (long i = 0; i < count; i++) {
long j = i % 3;
}
}
}