详解Class加载-初始化-Loading
类被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括7个阶段:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Usting)、卸载(Unloading)
加载Loading
- 通过一个类的全限定名来获取类的二进制字节流
并未指定总哪获取,怎么获取。所以字节流可以是存储在硬盘上的文件,可以是运行时动态生成的二进制字节流,可以是有其他文件生成的(JSP对应的class文件)等等 - 将字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的访问入口
类加载器
所有的class都是被类加载器(ClassLoader)加载到内存的。
每一个类,都需要它的类加载器和自身来确定在JVM中的唯一性。
每个类加载器都有自己维护的类名称空间。
在判断两个类是否相等时,只有这两个类由同一个类加载器加载的前提下才有意义
启动类加载器(BootStrap ClassLoader)
扩展类加载器(Extension ClassLoader)
应用程序类加载器(Appliaction ClassLoader)
自定义类加载器
类加载器之间的层次关系,被称为双亲委派模型,一般采用组合关系来复用父加载器的代码
双亲委派模型
要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
双亲委派模型的工作过程:
类加载器收到类加载请求时,首先不会自己加载这个类,而是把这个请求委派给父类加载器去完成。所以所有的类加载请求都会传递到顶层的启动类加载器(BootStrap)中,只有父加载器无法完成加载请求时(加载器负责的加载范围中无法通过类的全限定名找到此类),子加载器才会尝试进行加载。
ClassLoader源码
findCache →parent.loadClass() → findClass()
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过了(每个类加载器都有自己维护的类名称空间)
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 如果仍未找到,则按顺序调用findClass
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
1. 从子到父,再从父到子的过程
2. 双亲委派模型对保证JAVA程序的稳定运作非常重要。
如果随意一个class文件能被任意加载器加载会导致系统中出现非常多的名字相同的类,会使得java类型体系中最基础的行为无法保证。
如Object类,java.lang.String 类由自定义类加载器加载。
4. 父加载器
父加载器不是“类加载器的加载器”,也不是“类加载器的父类加载器”
APP(应用程序类加载器)的加载器是BootStrap(启动类加载器),但它的父加载器是Extension(扩展类加载器)
从源码角度看。类加载器代码中,有一个parent对象,这个对象指定的加载器是谁,则这个类的父加载器就是谁
自定义类加载器
1. 什么时候需要使用到自定义的类加载器
加载自己指定目录下的class文件时。
热部署,动态加载class文件(网络传输的二进制流),java代码存储在数据库中时。
写框架写类库时都需要用到自定义的类加载器。如:spring,tomcat都有自定义的类加载器。
2. 自定义类加载器的实现
主要在于自定义实现findClass(name)方法。(设计模式:构造函数,模版方法)
/**
* Created by 刘绍 on 2020/2/2.
*/
public class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
File f = new File("c:/test/",name.replace(".","/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
//将文件流读取进字节数组中
while ((b = fis.read()) != 0) {
baos.write(b);
}
//转成二进制字节数组
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
//defineClass 将二进制流转成class对象
return defineClass(name,bytes,0,bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
public static void main(String[] args) throws Exception{
ClassLoader l = new MyClassLoader();
Class<?> aClass = l.loadClass("com.ls.jvm.LoadClassByHand01");
System.out.println(aClass);
}
}
lazyloading(lazyInitializing)
1. JVM并未规定什么时候加载class文件
2. 但是严格规定了什么时候需要进行初始化(Initializing)**五种情况**
1. new、getstatic、putstatic、invokestatic指令时(final变量除外)
这4条指令最常见的java代码:
- 使用new关键字实例化对象的时候
- 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类的静态方法的时候。
2. java.lang.reflect对类进行反射调用的时候
3. 初始化子类的时候,父类必须初始化
4. 当虚拟机启动时,被执行的主类必须初始化(main()方法的那个类)
5. 使用动态语言支持java.lang.invoke.MethodHandle解析结果为REF_getStatic、REP_pubStatic、REF_invokeStatic的方法句柄,该类必须初始化
混合模式
解释器 bytecode intepreter
JIT即时编译器 just in-Time compiler
JVM默认使用混合使用解释器 + 热点代码编译
**不是所有的代码都会被JIT进行即时编译的,如果是这样的话JAVA就变成完全不能跨平台了,jit会把你的代码编译成与平台相关的底层机器语言,就失去了跨平台能力**
1. 起始阶段采用解释执行
2. 热点代码检测 (-XX:CompileThreshold = 10000)
- 多次被调用的方法(方法计数器:监测方法执行频率)
- 多次被调用的循环(循环计数器:监测循环执行频率)
- 达到一定频率时,代码被编译成本地方法(Native)
-Xmixed 默认混合模式(开始解释执行,启动速度较快,对热点代码实行检测和编译)
-Xint 使用解释默认,使用解释模式,启动很快,执行稍慢
-Xcomp 使用纯编译模式,执行很快,当类文件很多的时候启动非常慢