diff --git "a/JVM\344\270\212\347\257\207\357\274\232\345\206\205\345\255\230\344\270\216\345\236\203\345\234\276\345\233\236\346\224\266\347\257\207/04-\350\231\232\346\213\237\346\234\272\346\240\210/README.md" "b/JVM\344\270\212\347\257\207\357\274\232\345\206\205\345\255\230\344\270\216\345\236\203\345\234\276\345\233\236\346\224\266\347\257\207/04-\350\231\232\346\213\237\346\234\272\346\240\210/README.md" index de7681d40ac6785e29b14272eabd35bd2a481a2c..31647ceeecf6b6f4c9ab80bab73cbe40ab1ec275 100644 --- "a/JVM\344\270\212\347\257\207\357\274\232\345\206\205\345\255\230\344\270\216\345\236\203\345\234\276\345\233\236\346\224\266\347\257\207/04-\350\231\232\346\213\237\346\234\272\346\240\210/README.md" +++ "b/JVM\344\270\212\347\257\207\357\274\232\345\206\205\345\255\230\344\270\216\345\236\203\345\234\276\345\233\236\346\224\266\347\257\207/04-\350\231\232\346\213\237\346\234\272\346\240\210/README.md" @@ -1,4 +1,4 @@ -> 笔记来源:[尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)](https://www.bilibili.com/video/BV1PJ411n7xZ "尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)") +> 笔记来源:[尚硅谷 JVM 全套教程,百万播放,全网巅峰(宋红康详解 java 虚拟机)](https://www.bilibili.com/video/BV1PJ411n7xZ "尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)") > > 同步更新:https://gitee.com/vectorx/NOTE_JVM > @@ -8,750 +8,495 @@ [toc] -# 1. 概述 +# 4. 虚拟机栈 -类加载器是JVM执行类加载机制的前提。 +## 4.1. 虚拟机栈概述 -**ClassLoader的作用:** +### 4.1.1. 虚拟机栈出现的背景 -ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。 +由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。 -![image-20210501102535142](https://img-blog.csdnimg.cn/img_convert/fb51cabb2218d857a809a59918c5beec.png) +优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。 -## 1.1. 大厂面试题 +### 4.1.2. 初步印象 -> 蚂蚁金服: -> -> 深入分析ClassLoader,双亲委派机制 -> -> 类加载器的双亲委派模型是什么?一面:双亲委派机制及使用原因 -> -> -> -> 百度: -> -> 都有哪些类加载器,这些类加载器都加载哪些文件? -> -> 手写一个类加载器Demo -> -> Class的forName(“java.lang.String”)和Class的getClassLoader()的Loadclass(“java.lang.String”)有什么区别? -> -> -> -> 腾讯: -> -> 什么是双亲委派模型? -> -> 类加载器有哪些? -> -> -> -> 小米: -> -> 双亲委派模型介绍一下 -> -> -> -> 滴滴: -> -> 简单说说你了解的类加载器一面:讲一下双亲委派模型,以及其优点 -> -> -> -> 字节跳动: -> -> 什么是类加载器,类加载器有哪些? -> -> -> -> 京东: -> -> 类加载器的双亲委派模型是什么? -> -> 双亲委派机制可以打破吗?为什么 +有不少 Java 开发人员一提到 Java 内存结构,就会非常粗粒度地将 JVM 中的内存区理解为仅有 Java 堆(heap)和 Java 栈(stack)?为什么? -## 1.2. 类加载器的分类 +### 4.1.3. 内存中的栈与堆 -类的加载分类:显式加载 vs 隐式加载 +栈是运行时的单位,而堆是存储的单位 -class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。 +- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。 +- 堆解决的是数据存储的问题,即数据怎么放,放哪里 -- 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。 -- 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。 +![image-20200705163928652](https://img-blog.csdnimg.cn/img_convert/2d195f3aafd8b1f426efad75f0a52478.png) -在日常开发以上两种方式一般会混合使用。 +### 4.1.4. 虚拟机栈基本内容 -```java -//隐式加载 -User user=new User(); -//显式加载,并初始化 -Class clazz=Class.forName("com.test.java.User"); -//显式加载,但不初始化 -ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent"); -``` - -## 1.3. 类加载器的必要性 +#### Java 虚拟机栈是什么? -一般情况下,Java开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。从以下几个方面说: +Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用,是线程私有的。 -- 避免在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时,手足无措。只有了解类加载器的 加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题 -- 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了。 -- 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。 +#### 生命周期 -## 1.4. 命名空间 +生命周期和线程一致 -**何为类的唯一性?** +#### 作用 -$\color{red}{对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。}$每一个类加载器,都拥有一个独立的类名称空间:$\color{red}{比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。}$否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。 +主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。 -**命名空间** +#### 栈的特点 -- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 +栈是一种快速有效的分配存储方式,访问速度仅次于罹序计数器。 -- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 +JVM 直接对 Java 栈的操作只有两个: -- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类 +- 每个方法执行,伴随着进栈(入栈、压栈) +- 执行结束后的出栈工作 -在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。 +对于栈来说不存在垃圾回收问题(栈存在溢出的情况) -## 1.5. 类加载机制的基本特征 +![image-20200705165025382](https://img-blog.csdnimg.cn/img_convert/d4e4445f4faee685dc98d54129344bb3.png) -双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如,Java中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。 +#### 面试题:开发中遇到哪些异常? -可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。 +**栈中可能出现的异常** -单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。 +Java 虚拟机规范允许Java 栈的大小是动态的或者是固定不变的。 -## 1.6. 类加载器之间的关系 +- 如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个StackOverflowError 异常。 -Launcher类核心代码 +- 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。 ```java -Launcher.ExtClassLoader var1; -try { - var1 = Launcher.ExtClassLoader.getExtClassLoader(); -} catch (IOException var10) { - throw new InternalError("Could not create extension class loader", var10); +public static void main(String[] args) { + test(); } - -try { - this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); -} catch (IOException var9) { - throw new InternalError("Could not create application class loader", var9); +public static void test() { + test(); } - -Thread.currentThread().setContextClassLoader(this.loader); +//抛出异常:Exception in thread"main"java.lang.StackoverflowError +//程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。 ``` -- **ExtClassLoader的Parent类是null** - -- **AppClassLoader的Parent类是ExtClassLoader** - -- **当前线程的ClassLoader是AppClassLoader** - -$\color{red}{注意,这里的Parent类并不是Java语言意义上的继承关系,而是一种包含关系}$ - -
- -# 2. 类的加载器分类 - -JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。 - -从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况: - -![image-20210501164413665](https://img-blog.csdnimg.cn/img_convert/0c43fb4a7da20038c8f56b42a1ddf802.png) - -- 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加戟器。 -- 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用。 +**设置栈内存大小** -父类加载器和子类加载器的关系: +我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度 ```java -class ClassLoader{ - ClassLoader parent;//父类加载器 - public ClassLoader(ClassLoader parent){ - this.parent = parent; +public class StackDeepTest{ + private static int count=0; + public static void recursion(){ + count++; + recursion(); } -} -class ParentClassLoader extends ClassLoader{ - public ParentClassLoader(ClassLoader parent){ - super(parent); - } -} -class ChildClassLoader extends ClassLoader{ - public ChildClassLoader(ClassLoader parent){ //parent = new ParentClassLoader(); - super(parent); + public static void main(String args[]){ + try{ + recursion(); + } catch (Throwable e){ + System.out.println("deep of calling="+count); + e.printstackTrace(); + } } } ``` -正是由于子类加载器中包含着父类加载器的引用,所以可以通过子类加载器的方法获取对应的父类加载器 +## 4.2. 栈的存储单位 -**注意:** +### 4.2.1. 栈中存储什么? -启动类加载器通过C/C++语言编写,而自定义类加载器都是由Java语言编写的,虽然扩展类加载器和应用程序类加载器是被jdk开发人员使用java语言来编写的,但是也是由java语言编写的,所以也被称为自定义类加载器 +每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。 -## 2.1. 引导类加载器 +在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。 -启动类加载器(引导类加载器,Bootstrap ClassLoader) +栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。 -- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。 +### 4.2.2. 栈运行原理 -- 它用来加载Java的核心库(JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。 +JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。 -- 并不继承自java.lang.ClassLoader,没有父加载器。 +在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。 -- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类 +执行引擎运行的所有字节码指令只针对当前栈帧进行操作。 -- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。 +如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。 - ![image-20210501170011811](https://img-blog.csdnimg.cn/img_convert/43431a1abff0e5f2bd7bfd20fe91e5f7.png) - ![image-20210501170038212](https://img-blog.csdnimg.cn/img_convert/5cd73234cd993408846ec7b4c2cb7238.png) - 使用-XX:+TraceClassLoading参数得到。 +![image-20200705203142545](https://img-blog.csdnimg.cn/img_convert/893126a7a33507917e11377fd3e4b639.png) -启动类加载器使用C++编写的?Yes! - - - C/C++:指针函数&函数指针、C++支持多继承、更加高效 - - Java:由C++演变而来,(C++)–版,单继承 - -```java -System.out.println("**********启动类加载器**********"); -// 获取BootstrapclassLoader能够加载的api的路径 -URL[] urLs = sun.misc.Launcher.getBootstrapcLassPath().getURLs(); -for (URL element : urLs) { - System.out.println(element.toExternalForm()); -} -// 从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器 -ClassLoader classLoader = java.security.Provider.class.getClassLoader(); -System.out.println(classLoader); -``` +不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。 -**执行结果:** -![image-20210501170425889](https://img-blog.csdnimg.cn/img_convert/c72286acf05c7f86d1ea24f74e0c2a1e.png) +如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。 -## 2.2. 扩展类加载器 - -扩展类加载器(Extension ClassLoader) - -- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。 - -- 继承于ClassLoader类 - -- 父类加载器为启动类加载器 - -- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。 - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/ba54af96e744eb99a9248d13e55a7e3c.png) +Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。 ```java -System.out.println("***********扩展类加载器***********"); -String extDirs =System.getProperty("java.ext.dirs"); -for (String path :extDirs.split( regex:";")){ - System.out.println(path); -} - -// 从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器 -lassLoader classLoader1 = sun.security.ec.CurveDB.class.getClassLoader(); -System.out.print1n(classLoader1); //sun.misc. Launcher$ExtCLassLoader@1540e19d +public class CurrentFrameTest{ + public void methodA(){ + system.out.println("当前栈帧对应的方法->methodA"); + methodB(); + system.out.println("当前栈帧对应的方法->methodA"); + } + public void methodB(){ + System.out.println("当前栈帧对应的方法->methodB"); + } ``` -**执行结果:** +### 4.2.3. 栈帧的内部结构 - ![img](https://img-blog.csdnimg.cn/img_convert/b21313ee65acb4f01f1d03b88529e9f5.png) +每个栈帧中存储着: -## 2.3. 系统类加载器 +- 局部变量表(Local Variables) +- 操作数栈(operand Stack)(或表达式栈) +- 动态链接(DynamicLinking)(或指向运行时常量池的方法引用) +- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义) +- 一些附加信息 -应用程序类加载器(系统类加载器,AppClassLoader) +![image-20200705204836977](https://img-blog.csdnimg.cn/img_convert/0ed2029b435d547547f32540077bb082.png) -- java语言编写,由sun.misc.Launcher$AppClassLoader实现 -- 继承于ClassLoader类 -- 父类加载器为扩展类加载器 -- 它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库 -- $\color{red}{应用程序中的类加载器默认是系统类加载器。}$ -- 它是用户自定义类加载器的默认父加载器 -- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器 +并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的 -![image-20210501171206453](https://img-blog.csdnimg.cn/img_convert/19dd9595afeb6543593ef14161fc1bb8.png) +![image-20200705205443993](https://img-blog.csdnimg.cn/img_convert/2451fa6f061c426179e3674bb6813e46.png) -## 2.4. 用户自定义类加载器 +## 4.3. 局部变量表(Local Variables) -用户自定义类加载器 +局部变量表也被称之为局部变量数组或本地变量表 -- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。 -- 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。 -- $\color{red}{通过类加载器可以实现非常绝妙的插件机制}$,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。 -- 同时,$\color{red}{自定义加载器能够实现应用隔离}$,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。 -- 自定义类加载器通常需要继承于ClassLoader。 +- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及 returnAddress 类型。 +- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题 -
+- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性的 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。 +- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。 +- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。 -# 3. 测试不同的类的加载器 +### 4.3.1. 关于 Slot 的理解 -每个Class对象都会包含一个定义它的ClassLoader的一个引用。 -**获取ClassLoader的途径** +- 局部变量表,最基本的存储单元是 Slot(变量槽) +- 参数值的存放总是在局部变量数组的 index0 开始,到数组长度-1 的索引结束。 +- 局部变量表中存放编译期可知的各种基本数据类型(8 种),引用类型(reference),returnAddress 类型的变量。 +- 在局部变量表里,32 位以内的类型只占用一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占用两个 slot。 -```java -// 获得当前类的ClassLoader -clazz.getClassLoader() -// 获得当前线程上下文的ClassLoader -Thread.currentThread().getContextClassLoader() -// 获得系统的ClassLoader -ClassLoader.getSystemClassLoader() -``` +- byte、short、char 在存储前被转换为 int,boolean 也被转换为 int,0 表示 false,非 0 表示 true。 +- JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值 -**说明:** +- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 slot 上 -- 站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加 - 载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载 - 器压根儿就不是一个Java类,因此在Java程序中只能打印出空值。 -- 数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器 - 来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型 - 是基本数据类型,数组类是没有类加载器的。 +- 如果需要访问局部变量表中一个 64bit 的局部变量值时,只需要使用前一个索引即可。(比如:访问 long 或 doub1e 类型变量) +- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 this 将会存放在 index 为 0 的 slot 处,其余的参数按照参数表顺序继续排列。 -```java -// 运行结果:null -String[] strArr = new String[6]; -System.out.println(strArr.getClass().getClassLoader()); +![image-20200705212454445](https://img-blog.csdnimg.cn/img_convert/866e014106701648c63c0b232066fced.png) -// 运行结果:sun.misc.Launcher$AppCLassLoader@18b4aac2 -ClassLoaderTest[] test=new ClassLoaderTest[1]; -System.out.println(test.getClass().getClassLoader()); +### 4.3.2. Slot 的重复利用 -// 运行结果:null -int[]ints =new int[2]; -System.out.println(ints.getClass().getClassLoader()); -``` - -**代码:** +栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。 ```java -public class ClassLoaderTest1{ - public static void main(String[] args) { - //获取系统该类加载器 - ClassLoader systemClassLoader=ClassLoader.getSystemCLassLoader(); - System.out.print1n(systemClassLoader);//sun.misc.Launcher$AppCLassLoader@18b4aac2 - //获取扩展类加载器 - ClassLoader extClassLoader =systemClassLoader.getParent(); - System.out.println(extClassLoader);//sun.misc. Launcher$ExtCLassLoader@1540e19d - //试图获取引导类加载器:失败 - ClassLoader bootstrapClassLoader =extClassLoader.getParent(); - System.out.print1n(bootstrapClassLoader);//null - - //################################## - try{ - ClassLoader classLoader =Class.forName("java.lang.String").getClassLoader(); - System.out.println(classLoader); - //自定义的类默认使用系统类加载器 - ClassLoader classLoader1=Class.forName("com.atguigu.java.ClassLoaderTest1").getClassLoader(); - System.out.println(classLoader1); - - //关于数组类型的加载:使用的类的加载器与数组元素的类的加载器相同 - String[] arrstr = new String[10]; - System.out.println(arrstr.getClass().getClassLoader());//null:表示使用的是引导类加载器 - - ClassLoaderTest1[] arr1 =new ClassLoaderTest1[10]; - System.out.println(arr1.getClass().getClassLoader());//sun.misc. Launcher$AppcLassLoader@18b4aac2 - - int[] arr2 = new int[10]; - System.out.println(arr2.getClass().getClassLoader());//null: - } catch (ClassNotFoundException e) { - e.printStackTrace(); +public class SlotTest { + public void localVarl() { + int a = 0; + System.out.println(a); + int b = 0; + } + public void localVar2() { + { + int a = 0; + System.out.println(a); } + //此时的就会复用a的槽位 + int b = 0; } } ``` -
- -# 4. ClassLoader源码解析 +### 4.3.3. 静态变量与局部变量的对比 -**ClassLoader与现有类的关系:** +参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。 -![image-20210501173110637](https://img-blog.csdnimg.cn/img_convert/00148a792adcf05932114dff408f82b5.png) +我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。 -除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。 - -## 4.1. ClassLoader的主要方法 - -抽象类ClassLoader的主要方法:(内部没有抽象方法) +和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。 ```java -public final ClassLoader getParent() +public void test(){ + int i; + System. out. println(i); +} ``` -返回该类加载器的超类加载器 +这样的代码是错误的,没有赋值不能够使用。 -```java -public Class loadClass(String name) throws ClassNotFoundException -``` +### 4.3.4. 补充说明 -加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回 ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。 +在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。 -```java -protected Class findClass(String name) throws ClassNotFoundException -``` +局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。 -查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。 +## 4.4. 操作数栈(Operand Stack) -- 在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类。但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。 +每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack) -- 需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的。$\color{red}{一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。}$ +操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop) -```java -protected final Class defineClass(String name, byte[] b,int off,int len) -``` +- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈 +- 比如:执行复制、交换、求和等操作 -根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。 +![image-20200706090618332](https://img-blog.csdnimg.cn/img_convert/7f911f276f834d820fa09f31dd63a74b.png) -- defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象。 - -- $\color{red}{defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象}$ - -**简单举例:** +代码举例 ```java -protected Class findClass(String name) throws ClassNotFoundException { - // 获取类的字节数组 - byte[] classData =getClassData(name); - if (classData == null) { - throw new ClassNotFoundException(); - } else{ - //使用defineClass生成class对象 - return defineClass(name,classData,θ,classData.length); - } +public void testAddOperation(){ + byte i = 15; + int j = 8; + int k = i + j; } ``` -```java -protected final void resolveClass(Class c) -``` - -链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。 - -```java -protected final Class findLoadedClass(String name) +字节码指令信息 + +```shell +public void testAddOperation(); + Code: + 0: bipush 15 + 2: istore_1 + 3: bipush 8 + 5: istore_2 + 6:iload_1 + 7:iload_2 + 8:iadd + 9:istore_3 + 10:return ``` -查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改。 - -```java -private final ClassLoader parent; -``` - -它也是一个ClassLoader的实例,这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理。 - -## 4.2. SecureClassLoader与URLClassLoader - -接着SecureClassLoader扩展了ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联。 - -前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。$\color{red}{在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类}$,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。 - -![image-20210501174730756](https://img-blog.csdnimg.cn/img_convert/53f634bada23b3400a303bfcc6e11a31.png) - -## 4.3. ExtClassLoader与AppClassLoader - -了解完URLClassLoader后接着看看剩余的两个类加载器,即拓展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。 - -sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的,其类主要类结构如下: - -![img](https://img-blog.csdnimg.cn/img_convert/a77b6bca10308e6b9be35b1b2dfc84bc.png) - -我们发现ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式,而AppClassLoader重载了loadClass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。 - -## 4.4. Class.forName()与ClassLoader.loadClass() - -**Class.forName()** - -- Class.forName():是一个静态方法,最常用的是Class.forName(String className); - -- 根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时,会执行类的初始化。 - - ```java - Class.forName("com.atguigu.java.Helloworld"); - ``` - -**ClassLoader.loadClass()** - -- ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法。 - -- 该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器。 - - ```java - Classloader cl = ......; cl.loadClass("com.atguigu.java.Helloworld"); - ``` - -
- -# 5. 双亲委派模型 - -## 5.1. 定义与本质 - -类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全。 - -**定义** - -如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。 - -**本质** +操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。 -规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。 +操作数栈就是 JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。 -![image-20210501175529542](https://img-blog.csdnimg.cn/img_convert/41cbfd9ba1da676a7f05dae0dac3abae.png) +每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的 Code 属性中,为 max_stack 的值。 -![img](https://img-blog.csdnimg.cn/img_convert/c1526b63e95852b48551b4f136ebbd6f.png) +栈中的任何一个元素都是可以任意的 Java 数据类型 -## 5.2. 优势与劣势 +- 32bit 的类型占用一个栈单位深度 +- 64bit 的类型占用两个栈单位深度 -**双亲委派机制优势** +操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问 -- 避免类的重复加载,确保一个类的全局唯一性 +如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令。 - $\color{red}{Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。}$ +操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。 -- 保护程序安全,防止核心API被随意篡改 +另外,我们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。 -**代码支持** +## 4.5. 代码追踪 -双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现。该接口的逻辑如下: - -(1)先在当前加载器的缓存中查找有无目标类,如果有,直接返回。 - -(2)判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name,false)接口进行加载。 - -(3)反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassorNull(name)接口,让引导类加载器进行加载。 - -(4)如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。 - -双亲委派的模型就隐藏在这第2和第3步中。 - -**举例** - -假设当前加载的是java.lang.Object这个类,很显然,该类属于JDK中核心得不能再核心的一个类,因此一定只能由引导类加载器进行加载。当]VM准备加载javaJang.Object时,JVM默认会使用系统类加载器去加载,按照上面4步加载的逻辑,在第1步从系统类的缓存中肯定查找不到该类,于是进入第2步。由于从系统类加载器的父加载器是扩展类加载器,于是扩展类加载器继续从第1步开始重复。由于扩展类加载器的缓存中也一定查找不到该类,因此进入第2步。扩展类的父加载器是null,因此系统调用findClass(String),最终通过引导类加载器进行加载。 - -**思考** - -如果在自定义的类加载器中重写java.lang.ClassLoader.loadClass(String)或java.lang.ClassLoader.loadclass(String,boolean)方法,抹去其中的双亲委派机制,仅保留上面这4步中的第l步与第4步,那么是不是就能够加载核心类库了呢? - -这也不行!因为JDK还为核心类库提供了一层保护机制。不管是自定义的类加载器,还是系统类加载器抑或扩展类加载器,最终都必须调用 java.lang.ClassLoader.defineclass(String,byte[],int,int,ProtectionDomain)方法,而该方法会执行preDefineClass()接口,该接口中提供了对JDK核心类库的保护。 - -**弊端** - -检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。 - -通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。 - -**结论** - -$\color{red}{由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已。}$比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Serylet规范推荐的一种做法。 - -## 5.3. 破坏双亲委派机制 - -双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。 - -在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。 - -**第一次破坏双亲委派机制** - -双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前一—即JDK1.2面世以前的“远古”时代。 - -由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,$\color{red}{为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性}$,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。上节我们已经分析过loadClass()方法,双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。 - -**第二次破坏双亲委派机制:线程上下文类加载器** - -双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题($\color{red}{越基础的类由越上层的加载器进行加载}$),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有$\color{red}{基础类型又要调用回用户的代码,那该怎么办呢?}$ +``` +public void testAddOperation() { + byte i = 15; + int j = 8; + int k = i + j; +} +``` -这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,$\color{red}{启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?}$(SPI:在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI) +使用 javap 命令反编译 class 文件:` javap -v 类名.class` -为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:$\color{red}{线程上下文类加载器(Thread Context ClassLoader)}$。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。 +```java +public void testAddoperation(); Code: 0: bipush 15 2: istore_1 3: bipush 8 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: return +``` -有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,$\color{red}{这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则}$,但也是无可奈何的事情。 ,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。 +![image-20200706093131621](https://img-blog.csdnimg.cn/img_convert/da41b01ad08238a2c8065552aaa7d676.png) -![img](https://img-blog.csdnimg.cn/img_convert/814235dcce5471c2a527e82bafcf21c7.png) +![image-20200706093251302](https://img-blog.csdnimg.cn/img_convert/6867c7a53a4faff29318783b46aef097.png) -默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。 +![image-20200706093646406](https://img-blog.csdnimg.cn/img_convert/f2d6c5d906914ebdca0a472fee33e047.png) -**第三次破坏双亲委派机制** +![image-20200706093751711](https://img-blog.csdnimg.cn/img_convert/6ef9c2eb8ac3395133950caf8ef80da3.png) -双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如:**代码热替换(Hot Swap)、模块热部署(Hot Deployment)**等 +![image-20200706093859191](https://img-blog.csdnimg.cn/img_convert/28113c3dc724bbffe677ea61b49e7c56.png) -IBM公司主导的JSR-291(即OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(osGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bund1e连同类加载器一起换掉以实现代码的热替换。在oSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。 +![image-20200706093921573](https://img-blog.csdnimg.cn/img_convert/f960c074fb4a6b1f35e862568ed23d04.png) -当收到类加载请求时,OSGi将按照下面的顺序进行类搜索: +![image-20200706094046782](https://img-blog.csdnimg.cn/img_convert/73a07a0e10bef2537b6da97a5348fee2.png) -1)$\color{red}{将以java.*开头的类,委派给父类加载器加载。}$ +![image-20200706094109629](https://img-blog.csdnimg.cn/img_convert/17309b8150015a8b359f2cb2ef78cb60.png) -2)$\color{red}{否则,将委派列表名单内的类,委派给父类加载器加载。}$ +程序员面试过程中,常见的 i++和++i 的区别,放到字节码篇章时再介绍。 -3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。 +## 4.6. 栈顶缓存技术(Top Of Stack Cashing)技术 -4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。 +前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。 -5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。 +由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。 -6)否则,查找Dynamic Import列表的Bundle,委派给对应Bund1e的类加载器加载。 +## 4.7. 动态链接(Dynamic Linking) -7)否则,类查找失败。 +动态链接、方法返回地址、附加信息 : 有些地方被称为帧数据区 -说明:只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的 +每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic 指令 -小结:这里,我们使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为,但这里“被破坏”并不一定是带有贬义的。只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新。 +在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 class 文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。 -正如:OSGi中的类加载器的设计不符合传统的双亲委派的类加载器架构,且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议,但对这方面有了解的技术人员基本还是能达成一个共识,认为**OSGi中对类加载器的运用是值得学习的,完全弄懂了OSGi的实现,就算是掌握了类加载器的精粹。** +![image-20200706101251847](https://img-blog.csdnimg.cn/img_convert/31f48e82748240d7694e542374e9d723.png) -## 5.4. 热替换的实现 +为什么需要运行时常量池呢? -热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。$\color{red}{热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中。}$基本上大部分脚本语言都是天生支持热替换的,比如:PHP,只要替换了PHP源文件,这种改动就会立即生效,而无需重启Web服务器。 +常量池的作用:就是为了提供一些符号和常量,便于指令的识别 -但对Java来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类。因此,在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader。 +## 4.8. 方法的调用:解析与分配 -注意:由不同ClassLoader加载的同名类属于不同的类型,不能相互转换和兼容。即两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类是完全不同的。 +在 JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关 -根据这个特点,可以用来模拟热替换的实现,基本思路如下图所示: +### 4.8.1. 静态链接 -![image-20210501182003439](https://img-blog.csdnimg.cn/img_convert/9ae876265c85af4e431932647993dab7.png) -
+当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接 -# 6. 沙箱安全机制 +### 4.8.2. 动态链接 -沙箱安全机制 +如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。 -- 保证程序安全 -- 保护Java原生的JDK代码 +静态链接和动态链接不是名词,而是动词,这是理解的关键。 -$\color{red}{Java安全模型的核心就是Java沙箱(sandbox)}$。什么是沙箱?沙箱是一个限制程序运行的环境。 +--- -沙箱机制就是将Java代码$\color{red}{限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问}$。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。 +对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。 -沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。 +### 4.8.3. 早期绑定 -所有的Java程序运行都可以指定沙箱,可以定制安全策略。 +早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。 -## 6.1. JDK1.0时期 +### 4.8.4. 晚期绑定 -在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于**沙箱(Sandbox)机制**。如下图所示JDK1.0安全模型 +如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。 -![image-20210501182608205](https://img-blog.csdnimg.cn/img_convert/71caf905a0ca13866e24419b7faa14ee.png) +--- -## 6.2. JDK1.1时期 +随着高级语言的横空出世,类似于 Java 一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特悄,那么自然也就具备早期绑定和晚期绑定两种绑定方式。 -JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。 +Java 中任何一个普通的方法其实都具备虚函数的特征,它们相当于 C++语言中的虚函数(C++中则需要使用关键字 virtual 来显式定义)。如果在 Java 程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字 final 来标记这个方法。 -因此在后续的Java1.1版本中,针对安全机制做了改进,增加了**安全策略**。允许用户指定代码对本地资源的访问权限。 +--- -如下图所示JDK1.1安全模型 +### 4.8.5. 虚方法和非虚方法 -![image-20210501182626963](https://img-blog.csdnimg.cn/img_convert/b93f0829e8340131a49738f8843307e4.png) +如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。 -## 6.3. JDK1.2时期 +静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法。 -在Java1.2版本中,再次改进了安全机制,增加了**代码签名**。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型: +在类加载的解析阶段就可以进行解析,如下是非虚方法举例: -![image-20210501182652378](https://img-blog.csdnimg.cn/img_convert/81919ed70977f2b57ef5a11481c68abc.png) +```java +class Father{ public static void print(String str){ System. out. println("father "+str); } private void show(String str){ System. out. println("father"+str); }}class Son extends Father{ public class VirtualMethodTest{ public static void main(String[] args){ Son.print("coder"); //Father fa=new Father(); //fa.show("atguigu.com"); } } +``` -## 6.4. JDK1.6时期 +虚拟机中提供了以下几条方法调用指令: -当前最新的安全机制实现,则引入了**域(Domain)**的概念。 +#### 普通调用指令: -虚拟机会把所有代码加载到不同的系统域和应用域。$\color{red}{系统域部分专门负责与关键资源进行交互}$,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型(jdk1.6) +- invokestatic:调用静态方法,解析阶段确定唯一方法版本 +- invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本 +- invokevirtual:调用所有虚方法 +- invokeinterface:调用接口方法 -![image-20210501182740197](https://img-blog.csdnimg.cn/img_convert/cf22059daec45f370ae3afb33b870c1f.png) -
+#### 动态调用指令: -# 7. 自定义类的加载器 +- invokedynamic:动态解析出需要调用的方法,然后执行 -## 7.1. 为什么要自定义类加载器? +前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic 指令则支持由用户确定方法版本。其中 invokestatic 指令和 invokespecial 指令调用的方法称为非虚方法,其余的(fina1 修饰的除外)称为虚方法。 -- $\color{red}{隔离加载类}$ +**关于 invokednamic 指令** - 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。 +- JVM 字节码指令集一直比较稳定,一直到 Java7 中才增加了一个 invokedynamic 指令,这是Java 为了实现「动态类型语言」支持而做的一种改进。 -- $\color{red}{修改类加载的方式}$ +- 但是在 Java7 中并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。直到 Java8 的 Lambda 表达式的出现,invokedynamic 指令的生成,在 Java 中才有了直接的生成方式。 - 类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载 +- Java7 中增加的动态语言类型支持的本质是对 Java 虚拟机规范的修改,而不是对 Java 语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在 Java 平台的动态语言的编译器。 -- $\color{red}{扩展加载源}$ +#### 动态类型语言和静态类型语言 - 比如从数据库、网络、甚至是电视机机顶盒进行加载 +动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。 -- $\color{red}{防止源码泄漏}$ +说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。 - Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。 +### 4.8.6. 方法重写的本质 -**常见的场景** +**Java 语言中方法重写的本质:** -- 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。这个方面的集大成者是JavaEE和OSGI、JPMS等框架。 -- 应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型。 +1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。 +2. 如果在类型 C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常。 +3. 否则,按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证过程。 +4. 如果始终没有找到合适的方法,则抛出 java.1ang.AbstractMethodsrror 异常。 -**注意** +**IllegalAccessError 介绍** -在一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性。但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情。在做Java类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常。 +程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。 -## 7.2. 实现方式 +### 4.8.7. 方法的调用:虚方法表 -Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。 +在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM 采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。 -在自定义ClassLoader的子类时候,我们常见的会有两种做法: +每个类中都有一个虚方法表,表中存放着各个方法的实际入口。 -- 方式一:重写loadClass()方法 -- 方式二:重写findclass()方法 +虚方法表是什么时候被创建的呢? -**对比** +虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。 -- 这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。 -- loadclass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。$\color{red}{因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构}$。同时,也避免了自己重写loadClass()方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。 -- 当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。 +举例 1: -**说明** +![image-20200706144954070](https://img-blog.csdnimg.cn/img_convert/79470fa420092e07a411e4e849f743b8.png) -- 其父类加载器是系统类加载器 -- JVM中的所有类加载都会使用java.lang.ClassLoader.loadClass(String)接口(自定义类加载器并重写java.lang.ClassLoader.loadClass(String)接口的除外),连JDK的核心类库也不能例外。 +举例 2: -
+```java +interface Friendly{ void sayHello(); void sayGoodbye(); }class Dog{ public void sayHello(){ } public String tostring(){ return "Dog"; }}class Cat implements Friendly { public void eat() { } public void sayHello() { } public void sayGoodbye() { } protected void finalize() { }}class CockerSpaniel extends Dog implements Friendly{ public void sayHello() { super.sayHello(); } public void sayGoodbye() { }} +``` -# 8. Java9新特性 +![image-20210509203351535](https://img-blog.csdnimg.cn/img_convert/2264627eaca3c5385a75101478565f5b.png) -为了保证兼容性,JDK9没有从根本上改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行,仍然发生了一些值得被注意的变动。 +## 4.9. 方法返回地址(return address) -1. 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。可以通过classLoader的新方法getPlatformClassLoader()来获取。 +存放调用该方法的 pc 寄存器的值。一个方法的结束,有两种方式: - JDK9时基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件),其中的Java类库就已天然地满足了可扩展的需求,那自然无须再保留\lib\ext目录,此前使用这个目录或者java.ext.dirs系统变量来扩展JDK功能的机制已经没有继续存在的价值了。 +- 正常执行完成 +- 出现未处理的异常,非正常退出 -2. 平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader。 +无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。 - 现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。 +当一个方法开始执行后,只有两种方式可以退出这个方法: -![img](https://img-blog.csdnimg.cn/img_convert/323cfcda53f98034ed15372c0ea43685.png) +1. 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口; + - 一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。 + - 在字节码指令中,返回指令包含 ireturn(当返回值是 boolean,byte,char,short 和 int 类型时使用),lreturn(Long 类型),freturn(Float 类型),dreturn(Double 类型),areturn。另外还有一个 return 指令声明为 void 的方法,实例初始化方法,类和接口的初始化方法使用。 +2. 在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。 -​ 如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK9及更高版本的JDK中崩溃。 +方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码 -3. 在Java9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。 -4. 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例。 -5. 类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。 +```shell +Exception table:from to target type4 16 19 any19 21 19 any +``` -![img](https://img-blog.csdnimg.cn/img_convert/cb23791a5fb1bf1a4c8a28d6a3179e84.png) +本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等,让调用者方法继续执行下去。 -![img](https://img-blog.csdnimg.cn/img_convert/ef9b83abcdb9f54d0f0ec7d15f0adc44.png) +正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。 -![img](https://img-blog.csdnimg.cn/img_convert/192fda50804d35e7d1b44dc61a65ede1.png) +## 4.10. 一些附加信息 -![img](https://img-blog.csdnimg.cn/img_convert/f07a455ec275a6503bfad070ae3d9ffb.png) +栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。 -**代码:** +## 4.11. 栈的相关面试题 -```java -public class ClassLoaderTest { - public static void main(String[] args) { - System.out.println(ClassLoaderTest.class.getClassLoader()); - System.out.println(ClassLoaderTest.class.getClassLoader().getParent()); - System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent()); - - //获取系统类加载器 - System.out.println(ClassLoader.getSystemClassLoader()); - //获取平台类加载器 - System.out.println(ClassLoader.getPlatformClassLoader()); - //获取类的加载器的名称 - System.out.println(ClassLoaderTest.class.getClassLoader().getName()); - } -} -``` +- 举例栈溢出的情况?(StackOverflowError) + - 通过 -Xss 设置栈的大小 +- 调整栈大小,就能保证不出现溢出么? + - 不能保证不溢出 +- 分配的栈内存越大越好么? + - 不是,一定时间内降低了 OOM 概率,但是会挤占其它的线程空间,因为整个空间是有限的。 +- 垃圾回收是否涉及到虚拟机栈? + - 不会 +- 方法中定义的局部变量是否线程安全? + - 具体问题具体分析。如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。 +| 运行时数据区 | 是否存在 Error | 是否存在 GC | +| :----------- | :------------- | :---------- | +| 程序计数器 | 否 | 否 | +| 虚拟机栈 | 是(SOE) | 否 | +| 本地方法栈 | 是 | 否 | +| 方法区 | 是(OOM) | 是 | +| 堆 | 是 | 是 | diff --git "a/JVM\344\270\255\347\257\207\357\274\232\345\255\227\350\212\202\347\240\201\344\270\216\347\261\273\347\232\204\345\212\240\350\275\275\347\257\207/01-class\346\226\207\344\273\266\347\273\223\346\236\204/README.md" "b/JVM\344\270\255\347\257\207\357\274\232\345\255\227\350\212\202\347\240\201\344\270\216\347\261\273\347\232\204\345\212\240\350\275\275\347\257\207/01-class\346\226\207\344\273\266\347\273\223\346\236\204/README.md" index 814140bc34d2157e4e0ce75557c7cb3a4575c428..ee0ed8f169aba64fa03ac10c64a0a8348be85dd1 100644 --- "a/JVM\344\270\255\347\257\207\357\274\232\345\255\227\350\212\202\347\240\201\344\270\216\347\261\273\347\232\204\345\212\240\350\275\275\347\257\207/01-class\346\226\207\344\273\266\347\273\223\346\236\204/README.md" +++ "b/JVM\344\270\255\347\257\207\357\274\232\345\255\227\350\212\202\347\240\201\344\270\216\347\261\273\347\232\204\345\212\240\350\275\275\347\257\207/01-class\346\226\207\344\273\266\347\273\223\346\236\204/README.md" @@ -1,4 +1,4 @@ -> 笔记来源:[尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)](https://www.bilibili.com/video/BV1PJ411n7xZ "尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)") +> 笔记来源:[尚硅谷 JVM 全套教程,百万播放,全网巅峰(宋红康详解 java 虚拟机)](https://www.bilibili.com/video/BV1PJ411n7xZ "尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)") > > 同步更新:https://gitee.com/vectorx/NOTE_JVM > @@ -8,9 +8,9 @@ [toc] -# 1. Class文件结构 +# 1. Class 文件结构 - ## 1.1. Class字节码文件结构 +## 1.1. Class 字节码文件结构 @@ -145,22 +145,21 @@
+## 1.2. Class 文件数据类型 -## 1.2. Class文件数据类型 - -| 数据类型 | 定义 | 说明 | -| :------- | :----------------------------------------------------------- | :----------------------------------------------------------- | -| 无符号数 | 无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。 | 其中无符号数属于基本的数据类型。 以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节 | -| 表 | 表是由多个无符号数或其他表构成的复合数据结构。 | 所有的表都以“_info”结尾。 由于表没有固定长度,所以通常会在其前面加上个数说明。 | +| 数据类型 | 定义 | 说明 | +| :------- | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | +| 无符号数 | 无符号数可以用来描述数字、索引引用、数量值或按照 utf-8 编码构成的字符串值。 | 其中无符号数属于基本的数据类型。 以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节 | +| 表 | 表是由多个无符号数或其他表构成的复合数据结构。 | 所有的表都以“\_info”结尾。 由于表没有固定长度,所以通常会在其前面加上个数说明。 | ## 1.3. 魔数 **Magic Number(魔数)** -- 每个Class文件开头的4个字节的无符号整数称为魔数(Magic Number) -- 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。 -- 魔数值固定为0xCAFEBABE。不会改变。 -- 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误: +- 每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number) +- 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的 Class 文件。即:魔数是 Class 文件的标识符。 +- 魔数值固定为 0xCAFEBABE。不会改变。 +- 如果一个 Class 文件不以 0xCAFEBABE 开头,虚拟机在进行文件校验的时候就会直接抛出以下错误: ```java Error: A JNI error has occurred, please check your installation and try again @@ -169,17 +168,15 @@ Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value - 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。 - - ## 1.4. 文件版本号 -紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。 +紧接着魔数的 4 个字节存储的是 Class 文件的版本号。同样也是 4 个字节。第 5 个和第 6 个字节所代表的含义就是编译的副版本号 minor_version,而第 7 个和第 8 个字节就是编译的主版本号 major_version。 -它们共同构成了class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M.m。 +它们共同构成了 class 文件的格式版本号。譬如某个 Class 文件的主版本号为 M,副版本号为 m,那么这个 Class 文件的格式版本号就确定为 M.m。 -版本号和Java编译器的对应关系如下表: +版本号和 Java 编译器的对应关系如下表: -### 1.4.1. Class文件版本号对应关系 +### 1.4.1. Class 文件版本号对应关系 | 主版本(十进制) | 副版本(十进制) | 编译器版本 | | ---------------- | ---------------- | ---------- | @@ -195,33 +192,32 @@ Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value | 54 | 0 | 1.10 | | 55 | 0 | 1.11 | -Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。 +Java 的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加 1。 -不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。(向下兼容) +不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的。目前,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行由高版本编译器生成的 Class 文件。否则 JVM 会抛出 java.lang.UnsupportedClassVersionError 异常。(向下兼容) -在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的JDK版本和生产环境中的JDK版本是否一致。 +在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的 JDK 版本和生产环境中的 JDK 版本是否一致。 -- 虚拟机JDK版本为1.k(k>=2)时,对应的class文件格式版本号的范围为45.0 - 44+k.0(含两端)。 +- 虚拟机 JDK 版本为 1.k(k>=2)时,对应的 class 文件格式版本号的范围为 45.0 - 44+k.0(含两端)。 ## 1.5. 常量池集合 -常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。 +常量池是 Class 文件中内容最为丰富的区域之一。常量池对于 Class 文件中的字段和方法解析也有着至关重要的作用。 -随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石。 +随着 Java 虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个 Class 文件的基石。 ![image-20210508233536076](https://img-blog.csdnimg.cn/img_convert/5c2a8d904287373990cffe9b82428daa.png) 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。 -常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。 - +常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池容量计数值(constant_pool_count)。与 Java 中语言习惯不一样的是,这个容量计数是从 1 而不是 0 开始的。 | 类型 | 名称 | 数量 | | :------------- | :------------------ | :---------------------- | | u2(无符号数) | constant_pool_count | 1 | | cp_info(表) | constant_pool | constant_pool_count - 1 | -由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。 +由上表可见,Class 文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。 - 常量池表项中,用于存放编译时期生成的各种字面量符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放 @@ -230,27 +226,26 @@ Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主 **constant_pool_count(常量池计数器)** - 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。 -- 常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项。 -- Demo的值为: +- 常量池容量计数值(u2 类型):从 1 开始,表示常量池中有多少项常量。即 constant_pool_count=1 表示常量池中有 0 个常量项。 +- Demo 的值为: ![image-20210508234020104](https://img-blog.csdnimg.cn/img_convert/a17ef03e0783c664a51491aafde85d2a.png) -其值为0x0016,掐指一算,也就是22。需要注意的是,这实际上只有21项常量。索引为范围是1-21。为什么呢? +其值为 0x0016,掐指一算,也就是 22。需要注意的是,这实际上只有 21 项常量。索引为范围是 1-21。为什么呢? -通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。 +通常我们写代码时都是从 0 开始的,但是这里的常量池却是从 1 开始,因为它把第 0 项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值 0 来表示。 ### 1.5.2. 常量池表 -constant_pool是一种表结构,以1 ~ constant_pool_count - 1为索引。表明了后面有多少个常量项。 +constant_pool 是一种表结构,以 1 ~ constant_pool_count - 1 为索引。表明了后面有多少个常量项。 常量池主要存放两大类常量:字面量(Literal)符号引用(Symbolic References) -它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte(标记字节、标签字节)。 - +它包含了 class 文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第 1 个字节作为类型标记,用于确定该项的格式,这个字节称为 tag byte(标记字节、标签字节)。 | 类型 | 标志(或标识) | 描述 | | :------------------------------- | :----------- | :--------------------- | -| CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 | | CONSTANT_Integer_info | 3 | 整型字面量 | | CONSTANT_Float_info | 4 | 浮点型字面量 | | CONSTANT_Long_info | 5 | 长整型字面量 | @@ -271,45 +266,45 @@ constant_pool是一种表结构,以1 ~ constant_pool_count - 1为索引。表 常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。如下表: -| 常量 | 具体的常量 | -| :------- | :------------------ | -| 字面量 | 文本字符串 | -| | 声明为final的常量值 | -| 符号引用 | 类和接口的全限定名 | -| | 字段的名称和描述符 | -| | 方法的名称和描述符 | +| 常量 | 具体的常量 | +| :------- | :-------------------- | +| 字面量 | 文本字符串 | +| | 声明为 final 的常量值 | +| 符号引用 | 类和接口的全限定名 | +| | 字段的名称和描述符 | +| | 方法的名称和描述符 | **全限定名** -com/atguigu/test/Demo这个就是类的全限定名,仅仅是把包名的“.“替换成”/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。 +com/atguigu/test/Demo 这个就是类的全限定名,仅仅是把包名的“.“替换成”/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。 **简单名称** -简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是add和num。 +简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的 add()方法和 num 字段的简单名称分别是 add 和 num。 **描述符** -描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表: +描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void 类型都用一个大写字符来表示,而对象类型则用字符 L 加对象的全限定名来表示,详见下表: | 标志符 | 含义 | | :----- | :-------------------------------------------- | -| B | 基本数据类型byte | -| C | 基本数据类型char | -| D | 基本数据类型double | -| F | 基本数据类型float | -| I | 基本数据类型int | -| J | 基本数据类型long | -| S | 基本数据类型short | -| Z | 基本数据类型boolean | -| V | 代表void类型 | +| B | 基本数据类型 byte | +| C | 基本数据类型 char | +| D | 基本数据类型 double | +| F | 基本数据类型 float | +| I | 基本数据类型 int | +| J | 基本数据类型 long | +| S | 基本数据类型 short | +| Z | 基本数据类型 boolean | +| V | 代表 void 类型 | | L | 对象类型,比如:`Ljava/lang/Object;` | | [ | 数组类型,代表一维数组。比如:`double[] is [D | -用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法java.lang.String tostring()的描述符为()Ljava/lang/String; ,方法int abc(int[]x, int y)的描述符为([II)I。 +用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法 java.lang.String tostring()的描述符为()Ljava/lang/String; ,方法 int abc(int[]x, int y)的描述符为([II)I。 **补充说明:** -虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息。因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。 +虚拟机在加载 Class 文件时才会进行动态链接,也就是说,Class 文件中不会保存各个方法和字段的最终内存布局信息。因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。 这里说明下符号引用和直接引用的区别与关联: @@ -318,28 +313,28 @@ com/atguigu/test/Demo这个就是类的全限定名,仅仅是把包名的“. #### Ⅱ. 常量类型和结构 -常量池中每一项常量都是一个表,J0K1.7之后共有14种不同的表结构数据。如下表格所示: +常量池中每一项常量都是一个表,J0K1.7 之后共有 14 种不同的表结构数据。如下表格所示: ![image-20210509001319088](https://img-blog.csdnimg.cn/img_convert/8266c05b4b1506d4c456b427b90b1b75.png) 根据上图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容(主要是字面量、符号引用)的。比如: -CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只是整型字面量信息。 +CONSTANT_Integer_info 是用来描述常量池中字面量信息的,而且只是整型字面量信息。 -标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。 +标志为 15、16、18 的常量项类型是用来支持动态语言调用的(jdk1.7 时才加入的)。 **细节说明:** -- CONSTANT_Class_info结构用于表示类或接口 -- CONSTAT_Fieldref_info、CONSTAHT_Methodref_infoF和lCONSTANIT_InterfaceMethodref_info结构表示字段、方汇和按口小法 -- CONSTANT_String_info结构用于表示示String类型的常量对象 -- CONSTANT_Integer_info和CONSTANT_Float_info表示4字节(int和float)的数值常量 -- CONSTANT_Long_info和CONSTAT_Double_info结构表示8字作(long和double)的数值常量 - - 在class文件的常最池表中,所行的a字节常借均占两个表成员(项)的空问。如果一个CONSTAHT_Long_info和CNSTAHT_Double_info结构在常量池中的索引位n,则常量池中一个可用的索引位n+2,此时常量池长中索引为n+1的项仍然有效但必须视为不可用的。 -- CONSTANT_NameAndType_info结构用于表示字段或方法,但是和之前的3个结构不同,CONSTANT_NameAndType_info结构没有指明该字段或方法所属的类或接口。 -- CONSTANT_Utf8_info用于表示字符常量的值 -- CONSTANT_MethodHandle_info结构用于表示方法句柄 -- CONSTANT_MethodType_info结构表示方法类型 -- CONSTANT_InvokeDynamic_info结构表示invokedynamic指令所用到的引导方法(bootstrap method)、引导方法所用到的动态调用名称(dynamic invocation name)、参数和返回类型,并可以给引导方法传入一系列称为静态参数(static argument)的常量。 +- CONSTANT_Class_info 结构用于表示类或接口 +- CONSTAT_Fieldref_info、CONSTAHT_Methodref_infoF 和 lCONSTANIT_InterfaceMethodref_info 结构表示字段、方汇和按口小法 +- CONSTANT_String_info 结构用于表示示 String 类型的常量对象 +- CONSTANT_Integer_info 和 CONSTANT_Float_info 表示 4 字节(int 和 float)的数值常量 +- CONSTANT_Long_info 和 CONSTAT_Double_info 结构表示 8 字作(long 和 double)的数值常量 + - 在 class 文件的常最池表中,所行的 a 字节常借均占两个表成员(项)的空问。如果一个 CONSTAHT_Long_info 和 CNSTAHT_Double_info 结构在常量池中的索引位 n,则常量池中一个可用的索引位 n+2,此时常量池长中索引为 n+1 的项仍然有效但必须视为不可用的。 +- CONSTANT_NameAndType_info 结构用于表示字段或方法,但是和之前的 3 个结构不同,CONSTANT_NameAndType_info 结构没有指明该字段或方法所属的类或接口。 +- CONSTANT_Utf8_info 用于表示字符常量的值 +- CONSTANT_MethodHandle_info 结构用于表示方法句柄 +- CONSTANT_MethodType_info 结构表示方法类型 +- CONSTANT_InvokeDynamic_info 结构表示 invokedynamic 指令所用到的引导方法(bootstrap method)、引导方法所用到的动态调用名称(dynamic invocation name)、参数和返回类型,并可以给引导方法传入一系列称为静态参数(static argument)的常量。 **解析方法:** @@ -347,55 +342,56 @@ CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只 ![image-20210509002525647](https://img-blog.csdnimg.cn/img_convert/f3485b5ca6cb750454230270021fc68a.png) -- 使用javap命令解析:javap-verbose Demo.class或jclasslib工具会更方便。 +- 使用 javap 命令解析:javap-verbose Demo.class 或 jclasslib 工具会更方便。 -**总结1:** +**总结 1:** -- 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。 -- 在常量池列表中,CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。 -- 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8编码,就可以知道其长度。 +- 这 14 种表(或者常量项结构)的共同点是:表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。 +- 在常量池列表中,CONSTANT_Utf8_info 常量项是一种使用改进过的 UTF-8 编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。 +- 这 14 种常量项结构还有一个特点是,其中 13 个常量项占用的字节固定,只有 CONSTANT_Utf8_info 占用字节不固定,其大小由 length 决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过 utf-8 编码,就可以知道其长度。 -**总结2:** +**总结 2:** -- 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。 -- 常量池中为什么要包含这些内容?Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载C1ass文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态链接的内容,在虚拟机类加载过程时再进行详细讲解 +- 常量池:可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用 Class 文件空间最大的数据项目之一。 +- 常量池中为什么要包含这些内容?Java 代码在进行 Javac 编译的时候,并不像 C 和 C++那样有“连接”这一步骤,而是在虚拟机加载 C1ass 文件的时候进行动态链接。也就是说,在 Class 文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态链接的内容,在虚拟机类加载过程时再进行详细讲解 ## 1.6. 访问标志 **访问标识(access_flag、访问标志、访问标记)** -在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。各种访问标记如下所示: +在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。各种访问标记如下所示: -| 标志名称 | 标志值 | 含义 | -| :------------- | ------ | :----------------------------------------------------------- | -| ACC_PUBLIC | 0x0001 | 标志为public类型 | -| ACC_FINAL | 0x0010 | 标志被声明为final,只有类可以设置 | -| ACC_SUPER | 0x0020 | 标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法) | -| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | -| ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | -| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应) | -| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | -| ACC_ENUM | 0x4000 | 标志这是一个枚举 | +| 标志名称 | 标志值 | 含义 | +| :------------- | ------ | :------------------------------------------------------------------------------------------------------------------------- | +| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | +| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | +| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2 之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法) | +| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | +| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | +| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应) | +| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | +| ACC_ENUM | 0x4000 | 标志这是一个枚举 | -类的访问权限通常为ACC_开头的常量。 +类的访问权限通常为 ACC\_开头的常量。 -每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如,若是public final的类,则该标记为ACC_PUBLIC | ACC_FINAL。 +每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的。比如,若是 public final 的类,则该标记为 ACC_PUBLIC | ACC_FINAL。 -使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。 +使用 ACC_SUPER 可以让类更准确地定位到父类的方法 super.method(),现代编译器都会设置并且使用这个标记。 **补充说明:** -1. 带有ACC_INTERFACE标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。 - - 如果一个class文件被设置了ACC_INTERFACE标志,那么同时也得设置ACC_ABSTRACT标志。同时它不能再设置ACC_FINAL、ACC_SUPER 或ACC_ENUM标志。 - - 如果没有设置ACC_INTERFACE标志,那么这个class文件可以具有上表中除ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。 -2. ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于Java SE 8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本号是多少,Java虚拟机都认为每个class文件均设置了ACC_SUPER标志。 - - ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的ACC_SUPER标志在由JDK1.0.2之前的编译器所生成的access_flags中是没有确定含义的,如果设置了该标志,那么0racle的Java虚拟机实现会将其忽略。 +1. 带有 ACC_INTERFACE 标志的 class 文件表示的是接口而不是类,反之则表示的是类而不是接口。 + - 如果一个 class 文件被设置了 ACC_INTERFACE 标志,那么同时也得设置 ACC_ABSTRACT 标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 标志。 + - 如果没有设置 ACC_INTERFACE 标志,那么这个 class 文件可以具有上表中除 ACC_ANNOTATION 外的其他所有标志。当然,ACC_FINAL 和 ACC_ABSTRACT 这类互斥的标志除外。这两个标志不得同时设置。 +2. ACC_SUPER 标志用于确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义。针对 Java 虚拟机指令集的编译器都应当设置这个标志。对于 Java SE 8 及后续版本来说,无论 class 文件中这个标志的实际值是什么,也不管 class 文件的版本号是多少,Java 虚拟机都认为每个 class 文件均设置了 ACC_SUPER 标志。 + + - ACC_SUPER 标志是为了向后兼容由旧 Java 编译器所编译的代码而设计的。目前的 ACC_SUPER 标志在由 JDK1.0.2 之前的编译器所生成的 access_flags 中是没有确定含义的,如果设置了该标志,那么 0racle 的 Java 虚拟机实现会将其忽略。 -3. ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。 +3. ACC_SYNTHETIC 标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。 -4. 注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。 +4. 注解类型必须设置 ACC_ANNOTATION 标志。如果设置了 ACC_ANNOTATION 标志,那么也必须设置 ACC_INTERFACE 标志。 -5. ACC_ENUM标志表明该类或其父类为枚举类型。 +5. ACC_ENUM 标志表明该类或其父类为枚举类型。 ## 1.7. 类索引、父类索引、接口索引 @@ -411,32 +407,32 @@ CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只 这三项数据来确定这个类的继承关系: - 类索引用于确定这个类的全限定名 -- 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.1ang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为e。 -- 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。 +- 父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.1ang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 e。 +- 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。 ### 1.7.1. this_class(类索引) -2字节无符号整数,指向常量池的索引。它提供了类的全限定名,如com/atguigu/java1/Demo。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口。 +2 字节无符号整数,指向常量池的索引。它提供了类的全限定名,如 com/atguigu/java1/Demo。this_class 的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为 CONSTANT_Class_info 类型结构体,该结构体表示这个 class 文件所定义的类或接口。 ### 1.7.2. super_class(父类索引) -2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/object类。同时,由于Java不支持多继承,所以其父类只有一个。 +2 字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是 java/lang/object 类。同时,由于 Java 不支持多继承,所以其父类只有一个。 -super_class指向的父类不能是final。 +super_class 指向的父类不能是 final。 ### 1.7.3. interfaces 指向常量池索引集合,它提供了一个符号引用到所有已实现的接口 -由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(当然这里就必须是接口,而不是类)。 +由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的 CONSTANT_Class(当然这里就必须是接口,而不是类)。 #### Ⅰ. interfaces_count(接口计数器) -interfaces_count项的值表示当前类或接口的直接超接口数量。 +interfaces_count 项的值表示当前类或接口的直接超接口数量。 #### Ⅱ. interfaces[](接口索引集合) -interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为interfaces_count。每个成员interfaces[i]必须为CONSTANT_Class_info结构,其中0 <= i < interfaces_count。在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。 +interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_count。每个成员 interfaces[i]必须为 CONSTANT_Class_info 结构,其中 0 <= i < interfaces_count。在 interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口。 ## 1.8. 字段表集合 @@ -446,20 +442,20 @@ interfaces[]中每个成员的值必须是对常量池表中某项的有效索 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 -它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。 +它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private 或 protected)、是类变量还是实例变量(static 修饰符)、是否是常量(final 修饰符)等。 **注意事项:** -- 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。 -- 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。 +- 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。 +- 在 Java 语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。 ### 1.8.1. 字段计数器 **fields_count(字段计数器)** -fields_count的值表示当前class文件fields表的成员个数。使用两个字节来表示。 +fields_count 的值表示当前 class 文件 fields 表的成员个数。使用两个字节来表示。 -fields表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。 +fields 表中每个成员都是一个 field_info 结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。 | 标志名称 | 标志值 | 含义 | 数量 | | :------------- | :--------------- | :--------- | :--------------- | @@ -473,41 +469,41 @@ fields表中每个成员都是一个field_info结构,用于表示该类或接 #### Ⅰ. 字段表访问标识 -我们知道,一个字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected)、static修饰符、final修饰符、volatile修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些: +我们知道,一个字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected)、static 修饰符、final 修饰符、volatile 修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些: | 标志名称 | 标志值 | 含义 | | :------------ | :----- | :------------------------- | -| ACC_PUBLIC | 0x0001 | 字段是否为public | -| ACC_PRIVATE | 0x0002 | 字段是否为private | -| ACC_PROTECTED | 0x0004 | 字段是否为protected | -| ACC_STATIC | 0x0008 | 字段是否为static | -| ACC_FINAL | 0x0010 | 字段是否为final | -| ACC_VOLATILE | 0x0040 | 字段是否为volatile | -| ACC_TRANSTENT | 0x0080 | 字段是否为transient | +| ACC_PUBLIC | 0x0001 | 字段是否为 public | +| ACC_PRIVATE | 0x0002 | 字段是否为 private | +| ACC_PROTECTED | 0x0004 | 字段是否为 protected | +| ACC_STATIC | 0x0008 | 字段是否为 static | +| ACC_FINAL | 0x0010 | 字段是否为 final | +| ACC_VOLATILE | 0x0040 | 字段是否为 volatile | +| ACC_TRANSTENT | 0x0080 | 字段是否为 transient | | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | -| ACC_ENUM | 0x4000 | 字段是否为enum | +| ACC_ENUM | 0x4000 | 字段是否为 enum | #### Ⅱ. 描述符索引 -描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符L加对象的全限定名来表示,如下所示: +描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)及代表无返回值的 void 类型都用一个大写字符来表示,而对象则用字符 L 加对象的全限定名来表示,如下所示: | 标志符 | 含义 | | :----- | :-------------------------------------------------- | -| B | 基本数据类型byte | -| C | 基本数据类型char | -| D | 基本数据类型double | -| F | 基本数据类型float | -| I | 基本数据类型int | -| J | 基本数据类型long | -| S | 基本数据类型short | -| Z | 基本数据类型boolean | -| V | 代表void类型 | +| B | 基本数据类型 byte | +| C | 基本数据类型 char | +| D | 基本数据类型 double | +| F | 基本数据类型 float | +| I | 基本数据类型 int | +| J | 基本数据类型 long | +| S | 基本数据类型 short | +| Z | 基本数据类型 boolean | +| V | 代表 void 类型 | | L | 对象类型,比如:`Ljava/lang/Object;` | | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D | #### Ⅲ. 属性表集合 -一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute_count中,属性具体内容存放在attributes数组中。 +一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在 attribute_count 中,属性具体内容存放在 attributes 数组中。 ```java // 以常量属性为例,结构为: @@ -518,37 +514,37 @@ ConstantValue_attribute{ } ``` -说明:对于常量属性而言,attribute_length值恒为2。 +说明:对于常量属性而言,attribute_length 值恒为 2。 ## 1.9. 方法表集合 methods:指向常量池索引集合,它完整描述了每个方法的签名。 -- 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private或protected),方法的返回值类型以及方法的参数信息等。 -- 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。 -- 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法<clinit>()和实例初始化方法<init>())。 +- 在字节码文件中,每一个 method_info 项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private 或 protected),方法的返回值类型以及方法的参数信息等。 +- 如果这个方法不是抽象的或者不是 native 的,那么字节码中会体现出来。 +- 一方面,methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods 表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法<clinit>()和实例初始化方法<init>())。 **使用注意事项:** -在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。 +在 Java 语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个 class 文件中。 -也就是说,尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和Java语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。 +也就是说,尽管 Java 语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和 Java 语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。 ### 1.9.1. 方法计数器 **methods_count(方法计数器)** -methods_count的值表示当前class文件methods表的成员个数。使用两个字节来表示。 +methods_count 的值表示当前 class 文件 methods 表的成员个数。使用两个字节来表示。 -methods表中每个成员都是一个method_info结构。 +methods 表中每个成员都是一个 method_info 结构。 ### 1.9.2. 方法表 **methods[](方法表)** -methods表中的每个成员都必须是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个method_info结构的access_flags项既没有设置ACC_NATIVE标志也没有设置ACC_ABSTRACT标志,那么该结构中也应包含实现这个方法所用的Java虚拟机指令。 +methods 表中的每个成员都必须是一个 method_info 结构,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志,那么该结构中也应包含实现这个方法所用的 Java 虚拟机指令。 -method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法 +method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法 方法表的结构实际跟字段表是一样的,方法表结构如下: @@ -573,23 +569,23 @@ method_info结构可以表示类和接口中定义的所有方法,包括实例 ## 1.10. 属性表集合 -方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。 +方法表集合之后的属性表集合,指的是 class 文件所携带的辅助信息,比如该 class 文件的源文件的名称。以及任何带有 RetentionPolicy.CLASS 或者 RetentionPolicy.RUNTIME 的注解。这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试,一般无须深入了解。 此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。 -属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。 +属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但 Java 虚拟机运行时会忽略掉它不认识的属性。 ### 1.10.1. 属性计数器 **attributes_count(属性计数器)** -attributes_count的值表示当前class文件属性表的成员个数。属性表中每一项都是一个attribute_info结构。 +attributes_count 的值表示当前 class 文件属性表的成员个数。属性表中每一项都是一个 attribute_info 结构。 ### 1.10.2. 属性表 **attributes[](属性表)** -属性表的每个项的值必须是attribute_info结构。属性表的结构比较灵活,各种不同的属性只要满足以下结构即可。 +属性表的每个项的值必须是 attribute_info 结构。属性表的结构比较灵活,各种不同的属性只要满足以下结构即可。 **属性的通用格式** @@ -601,30 +597,30 @@ attributes_count的值表示当前class文件属性表的成员个数。属性 **属性类型** -属性表实际上可以有很多类型,上面看到的Code属性只是其中一种,Java8里面定义了23种属性。下面这些是虚拟机中预定义的属性: - -| 属性名称 | 使用位置 | 含义 | -| :---------------------------------- | :----------------- | :----------------------------------------------------------- | -| Code | 方法表 | Java代码编译成的字节码指令 | -| ConstantValue | 字段表 | final关键字定义的常量池 | -| Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 | -| Exceptions | 方法表 | 方法抛出的异常 | -| EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | -| InnerClass | 类文件 | 内部类列表 | -| LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 | -| LocalVariableTable | Code属性 | 方法的局部变量描述 | -| StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器和处理目标方法的局部变量和操作数有所需要的类是否匹配 | -| Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | -| SourceFile | 类文件 | 记录源文件名称 | -| SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | -| Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 | -| LocalVariableTypeTable | 类 | 是哟很难过特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | -| RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | -| RuntimeInvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | -| RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象或方法 | -| RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象或方法 | -| AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 | -| BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方法限定符 | +属性表实际上可以有很多类型,上面看到的 Code 属性只是其中一种,Java8 里面定义了 23 种属性。下面这些是虚拟机中预定义的属性: + +| 属性名称 | 使用位置 | 含义 | +| :---------------------------------- | :----------------- | :-------------------------------------------------------------------------------------------- | +| Code | 方法表 | Java 代码编译成的字节码指令 | +| ConstantValue | 字段表 | final 关键字定义的常量池 | +| Deprecated | 类,方法,字段表 | 被声明为 deprecated 的方法和字段 | +| Exceptions | 方法表 | 方法抛出的异常 | +| EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | +| InnerClass | 类文件 | 内部类列表 | +| LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | +| LocalVariableTable | Code 属性 | 方法的局部变量描述 | +| StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器和处理目标方法的局部变量和操作数有所需要的类是否匹配 | +| Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | +| SourceFile | 类文件 | 记录源文件名称 | +| SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | +| Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 | +| LocalVariableTypeTable | 类 | 是哟很难过特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | +| RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | +| RuntimeInvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | +| RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象或方法 | +| RuntimeInvisibleParameterAnnotation | 方法表 | 作用与 RuntimeInvisibleAnnotations 属性类似,只不过作用对象或方法 | +| AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 | +| BootstrapMethods | 类文件 | 用于保存 invokeddynamic 指令引用的引导方法限定符 | 或者(查看官网) @@ -632,21 +628,21 @@ attributes_count的值表示当前class文件属性表的成员个数。属性 **部分属性详解** -**① ConstantValue属性** +**① ConstantValue 属性** -ConstantValue属性表示一个常量字段的值。位于field_info结构的属性表中。 +ConstantValue 属性表示一个常量字段的值。位于 field_info 结构的属性表中。 ```java ConstantValue_attribute{ u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index;//字段值在常量池中的索引,常量池在该索引处的项给出该属性表示的常量值。(例如,值是1ong型的,在常量池中便是CONSTANT_Long) -} +} ``` **② Deprecated 属性** -Deprecated 属性是在JDK1.1为了支持注释中的关键词@deprecated而引入的。 +Deprecated 属性是在 JDK1.1 为了支持注释中的关键词@deprecated 而引入的。 ```java Deprecated_attribute{ @@ -655,9 +651,9 @@ Deprecated_attribute{ } ``` -**③ Code属性** +**③ Code 属性** -Code属性就是存放方法体里面的代码。但是,并非所有方法表都有Code属性。像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了。Code属性表的结构,如下图: +Code 属性就是存放方法体里面的代码。但是,并非所有方法表都有 Code 属性。像接口或者抽象方法,他们没有具体的方法体,因此也就不会有 Code 属性了。Code 属性表的结构,如下图: | 类型 | 名称 | 数量 | 含义 | | :------------- | :--------------------- | :--------------- | :----------------------- | @@ -672,21 +668,21 @@ Code属性就是存放方法体里面的代码。但是,并非所有方法表 | u2 | attributes_count | 1 | 属性集合计数器 | | attribute_info | attributes | attributes_count | 属性集合 | -可以看到:Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。 +可以看到:Code 属性表的前两项跟属性表是一致的,即 Code 属性表遵循属性表的结构,后面那些则是他自定义的结构。 **④ InnerClasses 属性** -为了方便说明特别定义一个表示类或接口的Class格式为C。如果C的常量池中包含某个CONSTANT_Class_info成员,且这个成员所表示的类或接口不属于任何一个包,那么C的ClassFile结构的属性表中就必须含有对应的InnerClasses属性。InnerClasses属性是在JDK1.1中为了支持内部类和内部接口而引入的,位于ClassFile结构的属性表。 +为了方便说明特别定义一个表示类或接口的 Class 格式为 C。如果 C 的常量池中包含某个 CONSTANT_Class_info 成员,且这个成员所表示的类或接口不属于任何一个包,那么 C 的 ClassFile 结构的属性表中就必须含有对应的 InnerClasses 属性。InnerClasses 属性是在 JDK1.1 中为了支持内部类和内部接口而引入的,位于 ClassFile 结构的属性表。 -**⑤ LineNumberTable属性** +**⑤ LineNumberTable 属性** -LineNumberTable属性是可选变长属性,位于Code结构的属性表。 +LineNumberTable 属性是可选变长属性,位于 Code 结构的属性表。 -LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数。 +LineNumberTable 属性是用来描述 Java 源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数。 -- start_pc,即字节码行号;1ine_number,即Java源代码行号。 +- start_pc,即字节码行号;1ine_number,即 Java 源代码行号。 -在Code属性的属性表中,LineNumberTable属性可以按照任意顺序出现,此外,多个LineNumberTable属性可以共同表示一个行号在源文件中表示的内容,即LineNumberTable属性不需要与源文件的行一一对应。 +在 Code 属性的属性表中,LineNumberTable 属性可以按照任意顺序出现,此外,多个 LineNumberTable 属性可以共同表示一个行号在源文件中表示的内容,即 LineNumberTable 属性不需要与源文件的行一一对应。 ```java // LineNumberTable属性表结构: @@ -701,14 +697,14 @@ LineNumberTable_attribute{ } ``` -**⑥ LocalVariableTable属性** +**⑥ LocalVariableTable 属性** -LocalVariableTable是可选变长属性,位于Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在Code属性的属性表中,LocalVariableTable属性可以按照任意顺序出现。Code属性中的每个局部变量最多只能有一个LocalVariableTable属性。 +LocalVariableTable 是可选变长属性,位于 Code 属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在 Code 属性的属性表中,LocalVariableTable 属性可以按照任意顺序出现。Code 属性中的每个局部变量最多只能有一个 LocalVariableTable 属性。 -- start pc + length表示这个变量在字节码中的生命周期起始和结束的偏移位置(this生命周期从头e到结尾10) -- index就是这个变量在局部变量表中的槽位(槽位可复用) -- name就是变量名 -- Descriptor表示局部变量类型描述 +- start pc + length 表示这个变量在字节码中的生命周期起始和结束的偏移位置(this 生命周期从头 e 到结尾 10) +- index 就是这个变量在局部变量表中的槽位(槽位可复用) +- name 就是变量名 +- Descriptor 表示局部变量类型描述 ```java // LocalVariableTable属性表结构: @@ -726,13 +722,13 @@ LocalVariableTable_attribute{ } ``` -**⑦ Signature属性** +**⑦ Signature 属性** -Signature属性是可选的定长属性,位于ClassFile,field_info或method_info结构的属性表中。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。 +Signature 属性是可选的定长属性,位于 ClassFile,field_info 或 method_info 结构的属性表中。在 Java 语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会为它记录泛型签名信息。 -**⑧ SourceFile属性** +**⑧ SourceFile 属性** -SourceFile属性结构 +SourceFile 属性结构 | 类型 | 名称 | 数量 | 含义 | | :--- | :------------------- | :--- | :----------- | @@ -740,9 +736,8 @@ SourceFile属性结构 | u4 | attribute_length | 1 | 属性长度 | | u2 | sourcefile index | 1 | 源码文件素引 | -可以看到,其长度总是固定的8个字节。 +可以看到,其长度总是固定的 8 个字节。 **⑨ 其他属性** -Java虚拟机中预定义的属性有20多个,这里就不一一介绍了,通过上面几个属性的介绍,只要领会其精髓,其他属性的解读也是易如反掌。 - +Java 虚拟机中预定义的属性有 20 多个,这里就不一一介绍了,通过上面几个属性的介绍,只要领会其精髓,其他属性的解读也是易如反掌。