目录
10.不同的类加载器 除了读取二进制流的动作和范围不一样,后续的加载逻辑是否也不一样?
1.Java代码执行流程图
java 文件通过javac编译成.class文件,也就是字节码文件,然后由JVM加载字节码,运行时解释器将字节码解释为一行行的机器码来执行。在程序运行期间,即时编译器会针对热点代码将该部分热点代码编译成字节码,以获得更高的执行效率。因为编译要比解释快得多,在整个运行时,即时编译器和解释器相互配合,使Java程序能够达到和编译型语言一样的执行速度。
HotSpot是如何判断一段代码是否为热点代码的? 热点探测技术(基于采样,基于计数器)
2.Java 类的生命周期
类加载只包含加载,连接,初始化这三个部分 ,解析时灵活的,它可以在初始化之后进行,实现所谓的后期绑定。
加载:
1.通过类的全限定名(包名 + 类名),获取到该类的.class
文件(来源不仅仅是class文件,也可以是运行时通过反射拿到的代理类二进制字节流)的二进制字节流,注意了仅仅只有获得二进制流的操作程序员可以控制。
2.拿到整个二进制字节流之后,转化为方法区的运行时数据结构。
3.在堆中生成一个代表该类的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
注意数组直接在由JVM创建,不走类加载器。
验证:不是必须的,可以通过参数关掉,验证包含了很多个步骤,分散在各个阶段内
- 文件格式的验证,校验二进制的字节流是否正确解析,如果可以则进入内存的方法去进行存贮,这也是唯一一个对字节流进行检验的步骤。这说明这个校验发生在加载的那个步骤。
经过上面的步骤,方法区内存在了该class的静态结构,堆中也存在了该class的内存对象。但是并不代表JVM已经完全认可了整个类,如果程序想要使用整个类就必须进行链接,而链接的第一步就是进一步对整个类进行验证。
- 元数据验证,主要对元数据的数据类型进行校验,保证都符合java语言规范
- 字节码验证,确定程序语义合法,运行时不会危害虚拟机
- 符号引用验证,在解析阶段发生。
准备:
准备阶段是正式的为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法去内进行分配。所以知道了这些变量大致是啥了,就是静态变量。赋得是0值。赋真正得值是在调用类得构造器得时候才会进行,整个动作在初始化阶段才会进行。
解析:
是将常量池内的符号引用转换为直接引用的过程,当一个Java类A被编译成class之后呢,A不知道引用的B有没有被编译,而且此时B肯定是没有被加载的。所以A肯定不知道B的实际地址,此时先用一个字符串S来表示B的地址,S就称为符号引用。在运行时,如果A发生了类加载,到解析阶段发现B还没被加载就会触发B的加载,此时就会将A的符号引用替换为B的实际地址,也就是直接引用。如果A调用的B是一个具体的实现类就是静态解析,如果这里的B是一个接口或者抽象类,那么在解析的时候还是符号引用,到了运行时真正的被调用的时候,虚拟机栈中知道是哪个具体的实现类了,整个时候符号引用才被替换为直接引用,整个叫动态解析。(虚拟机是用过16个指令来知道进行动态解析的)用它来实现了上层的后期绑定,这也就是解析阶段有时候会发生在初始化阶段之后的原因了。
初始化:这里是判断代码有没有主动的资源初始化动作。 这个动作不是指的类的构造函数,而是class层面的,比如成员变量的赋值动作,静态变量的赋值动作。只有显示的new 才会调用的对象的构造函数进行实例化。
初始化就是执行类的构造器方法init
()的过程。若该类具有父类,jvm
会保证父类的init
先执行,然后在执行子类的init
。整个时候对静态常量的0值替换成真实的值,成员变量的赋值动作等等。
注意这个init()方法不是类的构造函数,他是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生。
3.类加载器
4.如何实现一个自定义的类加载器
重写ClassLoder的loadClass方法,不提倡这种方式,因为这样会破坏双亲委派的机制,提倡的做法是把自己的类加载逻辑写到findClass()方法中。
5.有哪些类加载器?
启动类加载器无法被java程序直接引用,用户在编写自定义加载器的时候,如果要把加载请求委派给引导类加载器,则直接使用null替代即可.
开发者可以使用扩展类加载器。
程序中默认的加载器就是应用加载器了。
6.什么是双亲委派模型?
除了顶层的类加载器,其他的类加载器都有自己的父亲加载器,这些加载器的父子关系不是以继承实现的,而是通过组合实现的。
7.双亲委派模型的工作过程?
如果一个类收到加载的请求,首先他不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,最终所有的请求都应该传送到顶层的加载器中,只有父亲加载器反馈自己无法完成这个加载请求,子加载器才尝试自己去加载。
8.双亲委派模型的好处?
JVM中一个类的唯一标识是:类加载器+类名
我们可以使用不同的类加载器去加载jar包,以此来做到类的隔离
Java类随着他的加载器一起具备了一种带有优先级的层次关系,比如java.lang.Object这个类,无论哪一个类加载要加载这个类,都会委派给最顶端的启动类加载器进行加载,这个就保证了在各种类加载器的环境中都是同一个类,因为就算是同一个类,如果被不同的类加载器加载,也会算两个独立的类,因为每个类加载器都有自己的独立的名称空间。
可以看到我自己写的User类是通过AppClassLoader加载的
9. 双亲委派模型的破坏:
双亲委派模型并不是一个强制性的模型,而是java设计者推荐给开发者的类加载器实现方式,所以他有被主动破坏的时候
1. 重写loadclass破坏了双亲委派模型,双亲委派的逻辑就是存在于这个方法内的。那为什么loadclass不设计成为final的呢?因为ClassLoader在1.0就已经存在了,双亲委派模型在1.2才加入。loadclass方法在之前就有人重写了。
补救措施:
第二次破坏:
java中涉及到的SPI加载动作时,我顶层的启动类加载器不认识这些由各种厂商实现的代码,所以就使用线程上下文类加载器去加载,这个线程上下文类加载器默认是应用程序类加载器,这种行为就是父类加载器请求子类加载器去完成类加载的动作,这个就违背了双亲委派模型的一般性原则。
第三次被破坏:
由于追求代码热替换,模块热部署导致的连同类加载器一起换掉去实现。
10.不同的类加载器 除了读取二进制流的动作和范围不一样,后续的加载逻辑是否也不一样?
我们认为除了BooStrap ClassLoader,其他的类加载器都继承自java.lang.ClassLoader,都是由这个类的defineClass进行后续处理。
11.遇到限定名一样的类,这么多类加载器会不会产生混乱?
越核心的类库越被上层的类加载器加载,而某限定名的类一旦被加载过,被动情况下就不会在加载相同限定的类名,这样就能够有效避免混乱。
12.类加载器的实际应用:类隔离技术
类隔离技术是为了解决依赖冲突而诞生的,它通过自定义类加载器打破双亲委派机制,设置一套自己的类加载逻辑,然后利用类加载传导规则实现了不同模块的类隔离。各模块都有自己的某个版本的jar包的加载器。
用自定义类加载器实现Java类隔离_GeorgiaStar的博客-CSDN博客_java类隔离机制
13.自定义类加载器实践:
package jvm.classLoader;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private File classPathFile;
private ClassLoader jdkClassLoader;
public MyClassLoader(ClassLoader parent) {
super(null);
String path = MyClassLoader.class.getResource("").getPath();
this.classPathFile = new File(path);
}
//重写loadClass打破双亲委派模型
// @Override
// public Class<?> loadClass(String name) throws ClassNotFoundException {
// Class result = null;
// try {
// //这里先使用jdkClassLoader来加载jdk自带的类
// result = jdkClassLoader.loadClass(name);
// } catch (Exception e) {
// //忽略
// }
// if (result != null) {
// return result;
// }
// // jdkClassLoader找不到类时,就自行加载
// String className = MyClassLoader.class.getPackage().getName() + "." + name;
// File classFile = new File(classPathFile, name.replace(".", "\\") + ".class");
String classPath = classPathMap.get(name);
File file = new File(classPath);
// if (!classFile.exists()) {
// throw new ClassNotFoundException();
// }
//
// byte[] classBytes = getClassData(classFile);
// if (classBytes == null || classBytes.length == 0) {
// throw new ClassNotFoundException();
// }
// return defineClass(classBytes, 0, classBytes.length);
//
// }
/**
* 重写findClass不会打破双亲委派模型
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String className = MyClassLoader.class.getPackage().getName() + "." + name;
if (classPathFile != null) {
//这里出现错误,classloader只能加载.class文件
File classFile = new File(classPathFile, name.replace(".", "\\") + ".class");
if (classFile.exists()) {
try (FileInputStream in = new FileInputStream(classFile);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] bytes = new byte[1024];
int len;
while ((len = in.read(bytes)) != -1) {
out.write(bytes, 0, len);
}
return defineClass(className, out.toByteArray(), 0, out.size());
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.findClass(name);
}
private byte[] getClassData(File file) {
try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[]{};
}
}
如何替换 JDK 的类
当 Java 的原生 API 不能满足需求时,比如修改 HashMap 类,就必须要使用到 Java 的 endorsed 技术。我们需要将自己的 HashMap 类,打包成一个 jar 包,然后放到 -Djava.endorsed.dirs 指定的目录中。注意类名和包名,应该和 JDK 自带的是一样的。但是,java.lang 包下面的类除外,因为这些都是特殊保护的。