目录
2.2.1 Java 字节码的字节码查看器:javap -v
4.1启动类加载器(引导类加载器,根加载器,Bootstrap)
4.3应用程序类加载器(系统类加载器,AppClassLoader)
JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
虚拟机可以分为系统虚拟机和程序虚拟机
这里重点看一下基本信息、常量池和方法
Magic魔数
主副版本号
主版本号不兼容会引发以下错误:
字节码文件中常量池的作用:避免相同的内容重复定义,节省空间。
我们看下面一个案例
- public class ConstantPoolTest2 {
- public static final String a1 = "abc";
- public static final String a2 = "abc";
- public static final String abc = "abc";
- public static void main(String[] args) {
- ConstantPoolTest2 constantPoolTest = new ConstantPoolTest2();
- }
- }
首先看字段,我们看a1的常量值实际上指向了常量池中的8号
这实际上是常量池中的一个String_info,但是里面并没有存储真正的字符串字面量,而是一个10号常量的索引
10号常量存储的就是真正的字符串字面量
为什么字段不直接存储常量池里字符串字面量的索引?而要先找String_info,然后再找字符串字面量
因为字节码文件被加载的时候会把常量池中String_info加载到字符串常量池中,所以String_info需要存一份引用
那为什么String_info里不直接存储字符串字面量,而是存一份索引?
字段中的变量名也可能要引用常量池里的字符串字面量,如果用String_info存储字符串字面量则不合理,因为字段中的变量名并不是一个字符串的对象
符号引用
字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中。
注意:
iinc x by y:将局部变量表的x号位置增加y
很明显操作数栈中的值一直都是0,只要istore,那么局部变量表中的值也会被覆盖,所以最终i为0
由于iinc指令在iload指令之前,所以i的最终值是1
补充——字节码指令大全:实战详解java反编译字节码(操作指令助记符)_字节码反编译-CSDN博客
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
详细请看官网:简介 | arthas (aliyun.com)
dashboard显示当前系统的实时数据面板,-i刷新实时数据的时间间隔 (ms),-n刷新实时数据的次数
dump已加载类的字节码文件到特定目录:
dump -d 特定目录 类的全限定名(即包名+类名)
反编译已加载类,得到源码:
jad 类的全限定名(即包名+类名)
对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。这样Java虚拟机就能很好地控制开发者访问数据的范围。
加载阶段过后,字节码文件就已经被读取到了内存中,并且会创建一个代表该类的Class
对象。
第一个环节是验证,验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。
准备阶段为静态变量(static)分配内存并设置初始值。准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。但注意如果字段是final
修饰的基本类型或者字符串常量(经过编译器优化),则会在准备阶段直接赋予最终值。
解析阶段主要是将常量池中的符号引用替换为直接引用。 符号引用就是在字节码文件中使用编号来访问常量池中的内容。 直接引用不再使用编号,而是使用内存中地址进行访问具体的数据。
初始化阶段会执行字节码文件中 clinit 部分的字节码指令。
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
以下几种方式会导致类的初始化:
我们可以在JVM设置里添加 -XX:+TraceClassLoading 参数,这样可以看到有哪些类被加载
通过测试以下程序可以发现
类被加载时不一定会被初始化,而是在需要初始化的时候才初始化。
类加载但不初始化的情况
面试题分析
clinit指令在以下情况下不会出现
当出现继承关系时
如果把new B02()去掉会怎么样呢?
数组的创建不会导致数组中元素的类进行初始化。
类加载器的作用
类加载器(ClassLoader)负责获取类的字节码并加载到内存中。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层 方法将byte[]转换成方法区和堆中的数据。
类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种:
启动类加载器是最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,无法在Java代码中直接获取到,且没有父加载器(这里形容的是父子关系的层次结构,并非继承关系),也没有继承java.lang.lassLoader类。
它主要负责加载由系统属性 “sun.boot.cass.path” 指定的路径下的核心类库(即
- public class Demo {
- public static void main(String[] args) {
- //Bootstrap 引导类加载器
- //打印为null,是因为Bootstrap是C++实现的
- ClassLoader classLoader = Object.class.getClassLoader();
- System.out.println(classLoader);
-
- //查看引导类加载器会加载那些jar包
- URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
- for (URL urL : urLs) {
- System.out.println(urL);
- }
- }
- }
注: JDK9是jdk.internal.loader.ClassLoaders$PlatformClassLoader类
注: JDK9是jdk.internal.loader.ClassLoaders$AppClassLoader类
三者之间是没有继承关系的,而是一种组合关系
双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载。
双亲委派机制的好处
我们可以看看下面的程序
这里我自定义了一个String类,并且类的全限定名和JDK内置的String类完全一样。但运行结果如下:
异常提示:在类 java.lang.String 中找不到 main 方法。
why?这里程序在执行时识别的是src中的java.lang.String,src就是classpath,因此会调用系统加载器。但根据双亲委派机制,系统加载器会逐层委派上层加载器来加载此类,在委派的时候,最上层的加载器是根加载器,即根加载器优先级最高。而根加载器能够在jre\lib\rt.jar包中找到一个重名的java.lang.String(即jdk自带的String),因此根据双亲委派最终会由最顶层的根加载器来执行jdk自带的java.lang.String。显然,jdk中的String并没有main()方法,因此报错找不到main()
Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
loadClass默认实现如下:
再看看loadClass(String name, boolean resolve)函数,双亲委派机制的核心代码就位于这里
从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:
整个调用过程如下图所示:
在自定义ClassLoader的子类时候,我们常见的会有两种做法:
实战代码如下:
- public class MyClassLoader extends ClassLoader {
-
- private String root;
- @Override
- protected Class> findClass(String name) throws ClassNotFoundException {
-
- byte[] classData = loadClassData(name);
- if (classData == null) {
- throw new ClassNotFoundException();
- } else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
-
- private byte[] loadClassData(String className) {
-
- String fileName = root + File.separatorChar
- + className.replace('.', File.separatorChar) + ".class";
- try {
- InputStream ins = Files.newInputStream(Paths.get(fileName));
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int bufferSize = 1024;
- byte[] buffer = new byte[bufferSize];
- int length = 0;
- while ((length = ins.read(buffer)) != -1) {
-
- baos.write(buffer, 0, length);
- }
- return baos.toByteArray();
- } catch (IOException e) {
-
- e.printStackTrace();
- }
- return null;
- }
- public String getRoot() {
-
- return root;
- }
- public void setRoot(String root) {
-
- this.root = root;
- }
- public static void main(String[] args) {
-
- MyClassLoader classLoader = new MyClassLoader();
- classLoader.setRoot("D:\\");
- Class> testClass = null;
- try {
- //需要为com.字节码文件.classloader.A 格式,否则defineClass方法会抛异常
- testClass = classLoader.loadClass("com.字节码文件.classloader.A");
- Object object = testClass.newInstance();
- System.out.println(object.getClass().getClassLoader());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
注意
AppClassLoader和ExtClassLoader是Launcher的静态内部类,在程序启动时JVM会创建Launcher对象,Launcher构造器会同时会创建扩展类加载器和应用类加载器。
DriverManage使用SPI机制,最终加载jar包中对应的驱动类。
SPI中是如何获取到应用程序类加载器的?
SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。
我认为此案例中并没有打破双亲委派机制
JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制。
JDK8及之前的类加载器
JDK8及之前的版本中,扩展类加载器和应用程序类加载器的源码位于rt.jar包中的sun.misc.Launcher.java。
JDK8之后的类加载器
由于JDK9引入了module的概念,类加载器在设计上发生了很多变化。