肇庆市网站建设,最新的新闻,如何做彩票网站信息,深圳装修公司排名100强JVM概述
问题引出 你是否也遇到过这些问题#xff1f; 运行着的线上系统突然卡死#xff0c;系统无法访问#xff0c;甚至直接OOM#xff01;想解决线上JVM GC问题#xff0c;但却无从下手。新项目上线#xff0c;对各种JVM参数设置一脸茫然#xff0c;直接默认吧…JVM概述
问题引出 你是否也遇到过这些问题 运行着的线上系统突然卡死系统无法访问甚至直接OOM想解决线上JVM GC问题但却无从下手。新项目上线对各种JVM参数设置一脸茫然直接默认吧然后就JJ了每次面试之前都要重新背一遍JVM的一些原理概念性的东西然而面试官却经常问你在实际项目中如何调优JVM参数如何解决GC、OOM等问题一脸懵逼。 Java语言及Java生态圈 Oracle JDK与Open JDK迭代关系 1. Oracle JDK版本将每三年发布一次LTS版本而OpenJDK版本每三个月发布一次。 2. Oracle JDK将更多地关注稳定性它重视更多的企业级用户而OpenJDK经常发布以支持其他性能这可能会导致不稳定。 3. Oracle JDK支持长期发布的更改而Open JDK仅支持计划和完成下一个发行版。 4. Oracle JDK根据二进制代码许可协议获得许可而OpenJDK根据GPL v2许可获得许可。 使用Oracle平台时会产生一些许可影响。如Oracle 宣布的那样在没有商业许可的情况下在2019年1月之后发布的Oracle Java SE 8的公开更新将无法用于商业商业或生产用途。但是OpenJDK是完全开源的可以自由使用。 5. Oracle JDK的构建过程基于OpenJDK因此OpenJDK与Oracle JDK之间没有技术差异。 6. 顶级公司正在使用Oracle JDK例如Android StudioMinecraft和IntelliJ IDEA开发工具其中Open JDK不太受欢迎。 7. Oracle JDK具有Flight RecorderJava Mission Control和Application Class-Data Sharing功能Open JDK具有Font Renderer功能这是OpenJDK与Oracle JDK之间的显着差异。 8. Oracle JDK具有良好的GC选项和更好的渲染器而OpenJDK具有更少的GC选项并且由于其包含自己的渲染器的分布因此具有较慢的图形渲染器选项。 9. 在响应性和JVM性能方面Oracle JDK与OpenJDK相比提供了更好的性能。 10. 与OpenJDK相比Oracle JDK的开源社区较少OpenJDK社区用户的表现优于Oracle JDK发布的功能以提高性能。 11. 如果使用Oracle JDK会产生许可影响而OpenJDK没有这样的问题并且可以以任何方式使用以满足完全开源和免费使用。 12. Oracle JDK在运行JDK时不会产生任何问题而OpenJDK在为某些用户运行JDK时会产生一些问题。 13. 根据使用方的使用和许可协议现有应用程序可以从Oracle JDK迁移到Open JDK反之亦然。 14. Oracle JDK将从其10.0.X版本将收费用户必须付费或必须依赖OpenJDK才能使用其免费版本。 15. Oracle JDK不会为即将发布的版本提供长期支持用户每次都必须通过更新到最新版本获得支持来获取最新版本。 16. Oracle JDK以前的1.0版以前的版本是由Sun开发的后来被Oracle收购并为其他版本维护而OpenJDK最初只基于Java SDK或JDK版本7。 17. Oracle JDK发布时大多数功能都是开源的其中一些功能免于开源并且根据Sun的许可授权而OpenJDK发布了所有功能如开源和免费。 18. Oracle JDK完全由Oracle公司开发而Open JDK项目由IBMAppleSAP AGRedhat等顶级公司加入和合作。 JDK与JVM是什么关系 如何理解Java是跨平台的语言 “write once, run anywhere.” 即 “一处编写处处运行” 当Java源代码成功编译成字节码后如果想在不同的平台上面运行则无须再次编译这个优势不再那么吸引人了。Python、PHP、Perl、Ruby、Lisp等有强大的解释器。跨平台似乎已经快成为一门语言必选的特性。 如何理解JVM跨语言的平台 Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的它只关心“字节码”文件。 Java不是最强大的语言但是JVM是最强大的虚拟机。 Java不存在内存溢出内存泄漏 java (c)--; 垃圾收集机制为我们打理了很多繁琐的工作大大提高了开发的效率但是垃圾收集也不是万能的懂得JVM内部的内存结构、工作机制是设计高扩展性应用和诊断运行时问题的基础也是Java工程师进阶的必备能力。 Java发展的几个重大事件 2000年JDK 1.3发布Java HotSpot Virtual Machine正式发布成为Java的默认虚拟机。2002年JDK 1.4发布古老的Classic虚拟机退出历史舞台。2003年年底Java平台的Scala正式发布同年Groovy也加入了 Java阵营。2006年JDK 6发布。同年Java开源并建立了 OpenJDK。顺理成章Hotspot虚拟机也成为了 OpenJDK中的默认虚拟机。2007年Java平台迎来了新伙伴Clojure。2008 年Oracle 收购了 BEA,得到了 JRockit 虚拟机。2009年Twitter宣布把后台大部分程序从Ruby迁移到Scala这是Java平台的又一次大规模应用。2010年Oracle收购了Sun获得Java商标和最具价值的HotSpot虚拟机。此时Oracle拥有市场占用率最高的两款虚拟机HotSpot和JRockit并计划在未来对它们进行整合HotRockit. JCP组织管理Java语言2011年JDK7发布。在JDK 1.7u4中正式启用了新的垃圾回收器G1。2017年JDK9发布。将G1设置为默认GC替代CMS (被标记为Deprecated)同年IBM的J9开源形成了现在的Open J9社区2018年Android的Java侵权案判决Google赔偿Oracle计88亿美元同年JDK11发布LTS版本的JDK,发布革命性的ZGC,调整JDK授权许可2019年JDK12发布加入RedHat领导开发的Shenandoah GC JVM的生命周期 虚拟机的启动Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的这个类是由虚拟机的具体实现指定的。 虚拟机的退出有如下的几种情况 某线程调用Runtime类或System类的exit方法或 Runtime类的halt方法并且Java安全管理器也允许这次exit或halt操作。程序正常执行结束程序在执行过程中遇到了异常或错误而异常终止由于操作系统出现错误而导致Java虚拟机进程终止 重点说下HotSpot? SUN的JDK版本从1.3.1开始运用HotSpot虚拟机 2006年底开源主要使用C实现JNI接口部分用C实现。HotSpot是较新的Java虚拟机使用JIT(Just in Time)编译器可以大大提高Java运行的性能。 Java原先是把源代码编译为字节码在虚拟机执行这样执行速度较慢。而HotSpot将常用的部分代码编译为本地(原生native)代码这样显着提高了性能。 HotSpot JVM 参数可以分为规则参数(standard options)和非规则参数(non-standard options)。 规则参数相对稳定在JDK未来的版本里不会有太大的改动。 非规则参数则有因升级JDK而改动的可能。 VM的架构与知识脉络图 面试题 什么是Java虚拟机墨迹天气JVM的组成凡预科技、杭州比智公司JVM的组成字节跳动什么是Java虚拟机(JVM)为什么要使用JVM ! Japanese Vedios Man 阿里 虚拟机指以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统 是物理机的软件实现。常用的虚拟机有VMWareVisual BoxJava Virtual MachineJava虚拟机简称JVM说说Java虚拟机的体系结构阿里 JVM架构图 这个架构可以分成三层看 最上层javac编译器将编译好的字节码class文件通过java 类装载器执行机制把对象或class文件存放在 jvm划分内存区域。中间层称为Runtime Data Area主要是在Java代码运行时用于存放数据的从左至右为方法区(永久代、元数据区)、堆(共享,GC回收对象区域)、栈、程序计数器、寄存器、本地方法栈(私有)。最下层解释器、JIT(just in time)编译器和 GCGarbage Collection垃圾回收器 JVM知识脉络 字节码文件概述
字节码文件是跨平台的吗 Java 虚拟机不和包括 Java 在内的任何语言绑定它只与“Class 文件”这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发只要能将源文件编译为正确的Class文件那么这种语言就可以在Java虚拟机上执行。可以说统一而强大的Class文件结构就是Java虚拟机的基石、桥梁。 想要让一个Java程序正确地运行在JVM中Java源码就必须要被编译为符合JVM规范的字节码。
官网Java SE规范 所有的JVM全部遵守Java虚拟机规范也就是说所有的JVM环境都是一样的这样一来字节码文件可以在各种JVM上运行。从Java虚拟机的角度看通过Class文件可以让更多的计算机语言支持Java虚拟机平台。因此Class文件结构不仅仅是Java虚拟机的执行入口更是Java生态圈的基础和核心。 class文件里是什么 源代码经过编译器编译之后便会生成一个字节码文件字节码是一种二进制的类文件它的内容是JVM的指令而不像C、C经由编译器直接生成机器码。 随着Java平台的不断发展在将来Class文件的内容也一定会做进一步的扩充但是其基本的格式和结构不会做重大调整。 能介绍下生成class文件的编译器吗 1. 从位置上理解 前端编译器 vs 后端编译器 半编译半解释型语言 javac ... java ... 2. 前端编译器的种类 Java源代码的编译结果是字节码那么肯定需要有一种编译器能够将Java源码编译为字节码承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器。 HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码其实只要编译结果符合JVM规范都可以被JVM所识别即可。 在Java的前端编译器领域除了javac之外还有一种被大家经常用到的前端编译器那就是内置在Eclipse中的ECJ (Eclipse Compiler for Java)编译器。和Javac的全量式编译不同ECJ是一种增量式编译器。 在Eclipse中当开发人员编写完代码后使用“CtrlS”快捷键时ECJ编译器所釆取的编译方案是把未编译部分的源码逐行进行编译而非每次都全量编译。因此ECJ的编译效率会比javac更加迅速和高效当然编译质量和javac相比大致还是一样的。ECJ不仅是Eclipse的默认内置前端编译器在Tomcat中同样也是使用ECJ编译器来编译jsp文件。由于ECJ编译器是釆用GPLv2的开源协议进行源代码公开所以大家可以登录eclipse官网下载ECJ编译器的源码进行二次开发。默认情况下IntelliJ IDEA 使用 javac 编译器。(还可以自己设置为AspectJ编译器 ajc) 3. 前端编译器的任务 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。 javac编译器的编译步骤 javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤分别是词法解析、语法解析、语义解析以及生成字节码。 大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前都需要经过上图中的各个步骤。 目前前端编译器的局限性 前端编译器并不会直接涉及编译优化等方面的技术而是将这些具体优化细节移交给HotSpot的JIT编译器负责。 AOT(静态提前编译器Ahead Of Time Compiler) jdk9引入了AOT编译器(静态提前编译器Ahead Of Time Compiler)Java 9 引入了实验性 AOT 编译工具jaotc。它借助了 Graal 编译器将所输入的 Java 类文件转换为机器码并存放至生成的动态共享库之中。 所谓 AOT 编译是与即时编译相对立的一个概念。我们知道即时编译指的是在程序的运行过程中将字节码转换为可在硬件上直接运行的机器码并部署至托管环境中的过程。而 AOT 编译指的则是在程序运行之前便将字节码转换为机器码的过程。 .java - .class - .so最大好处 Java虚拟机加载已经预编译成二进制库可以直接执行。不必等待即时编译器的预热减少Java应用给人带来“第一次运行慢”的不良体验。 缺点 破坏了java“一次编译到处运行”必须为每个不同硬件、OS编译对应的发行包。 降低了Java链接过程的动态性加载的代码在编译期就必须全部已知。 那些类型对应有Class的对象
1class外部类成员(成员内部类静态内部类)局部内部类匿名内部类
2interface接口
3[]数组
4enum枚举
5annotation注解interface
6primitive type基本数据类型
7void
Test
public void test(){Class c1 Object.class;Class c2 Comparable.class;Class c3 String[].class;Class c4 int[][].class;Class c5 ElementType.class;Class c6 Override.class;Class c7 int.class;Class c8 void.class;Class c9 Class.class;int[] a new int[10];int[] b new int[100];Class c10 a.getClass();Class c11 b.getClass();// 只要元素类型与维度一样就是同一个ClassSystem.out.println(c10 c11);
} 字节码指令 面试题 实例1 public class ByteCodeInterview {//面试题 i和i有什么区别Testpublic void test1(){int i 10;i;//i;System.out.println(i);}Testpublic void test2(){int i 10;i i;System.out.println(i);}Testpublic void test3(){int i 2;i * i;System.out.println(i);}Testpublic void test4(){int k 10;k k (k) (k);System.out.println(k);}//包装类对象的缓存问题Testpublic void test5(){
// Integer x 5;
// int y 5;Integer i1 10;Integer i2 10;System.out.println(i1 i2);Integer i3 128;Integer i4 128;System.out.println(i3 i4);Boolean b1 true;Boolean b2 true;System.out.println(b1 b2);}Testpublic void test6(){String str new String(hello) new String(world);String str1 helloworld;System.out.println(str str1);}
} 实例2 class Father {int x 10;public Father() {this.print();x 20;}public void print() {System.out.println(Father.x x);}
}class Son extends Father {int x 30;public Son() {this.print();x 40;}public void print() {System.out.println(Son.x x);}
}public class SonTest {public static void main(String[] args) {Father f new Son();System.out.println(f.x);}
} 什么是字节码指令 Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码opcode以及跟随其后的零至多个代表此操作所需参数的操作数operand所构成。虚拟机中许多指令并不包含操作数只有一个操作码。 比如 如何解读class文件
方式一一个一个二进制的看。这里用到的是Notepad,需要安装一个HEX-Editor插件或者使用Binary Viewer 方式二使用javap指令jdk自带的反解析工具
方式三使用IDEA插件jclasslib 或jclasslib bytecode viewer客户端工具。可视化更好 或者 Class文件结构细节
官网文档类文件格式 Class 类的本质 任何一个Class文件都对应着唯一一个类或接口的定义信息但反过来说Class文件实际上它并不一定以磁盘文件的形式存在。Class 文件是一组以8位字节为基础单位的二进制流。 Class文件格式 Class 的结构不像 XML 等描述语言由于它没有任何分隔符号。所以在其中的数据项无论是字节顺序还是数量都是被严格限定的哪个字节代表什么含义长度是多少先后顺序如何都不允许改变。 Class 文件格式采用一种类似于 C语言结构体的方式进行数据存储这种结构中只有两种数据类型无符号数和表。 无符号数属于基本的数据类型以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的复合数据类型所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据整个 Class 文件本质上就是一张表。 由于表没有固定长度所以通常会在其前面加上个数说明 代码举例 public class Demo {private int num 1;public int add(){num num 2;return num;}
} 对应的字节码文件 换句话说充分理解了每一个字节码文件的细节自己也可以反编译出Java源文件来。 class文件结构细节概述 Class文件的结构并不是一成不变的随着Java虚拟机的不断发展总是不可避免地会对Class文件结构做出一些调整但是其基本结构和框架是非常稳定的。
Class文件的总体结构如下
魔数Class文件版本常量池访问标识(或标志)类索引父类索引接口索引集合字段表集合方法表集合属性表集合 这是一张Java字节码总的结构表我们按照上面的顺序逐一进行解读就可以了。 class文件的魔数是什么
Magic Number魔数class文件的标志
每个 Class 文件开头的4个字节的无符号整数称为魔数Magic Number它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即魔数是Class文件的标识符。魔数值固定为0xCAFEBABE。不会改变。
如果一个Class文件不以0xCAFEBABE开头虚拟机在进行文件校验的时候就会直接抛出以下错误 Error: A JNI error has occurred, please check your installation and try again Exception in thread main java.lang.ClassFormatError: Incompatible magic value 1885430635 in class file StringTest 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑因为文件扩展名可以随意地改动。 如何确保高版本的JVM可执行低版本的class文件
不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。 向下兼容在实际应用中由于开发环境和生产环境的不同可能会导致该问题的发生。因此需要我们在开发时特别注意开发编译的JDK版本和生产环境中的JDK版本是否一致。
class文件版本号
紧接着魔数的 4 个字节存储的是 Class 文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version而第7个和第8个字节就是编译的主版本号major_version。它们共同构成了class文件的格式版本号。譬如某个 Class 文件的主版本号为 M副版本号为 m那么这个Class 文件的格式版本号就确定为 M.m。版本号和Java编译器的对应关系如下表 Java 的版本号是从45开始的JDK 1.1之后的每个JDK大版本发布主版本号向上加1。虚拟机JDK版本为1.k k 2时对应的class文件格式版本号的范围为45.0 - 44k.0 含两端。 常量池class.文件的基石作用是
常量池存放所有常量
常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。常量池可以理解为Class文件之中的资源仓库它是Class文件结构中与其他项目关联最多的数据类型后面的很多数据类型都会指向此处也是占用Class文件空间最大的数据项目之一。常量池表项中用于存放编译时期生成的各种字面量和符号引用这部分内容将在类加载后进入方法区的运行时常量池中存放。 在版本号之后紧跟着的是常量池的数量以及若干个常量池表项。常量池中常量的数量是不固定的所以在常量池的入口需要放置一项u2类型的无符号数代表常量池容量计数值constant_pool_count。与Java中语言习惯不一样的是这个容量计数是从1而不是0开始的。 由上表可见Class文件使用了一个前置的容量计数器constant_pool_count加若干个连续的数据项constant_pool的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。 1-为什么需要常量池计数器 constant_pool_count 常量池计数器 由于常量池的数量不固定时长时短所以需要放置两个字节来表示常量池容量计数值。常量池容量计数值u2类型从1开始表示常量池中有多少项常量。即constant_pool_count1表示常量池中有0个常量项。Demo的值为 其值为0x0016,掐指一算也就是22。 需要注意的是这实际上只有21项常量。索引为范围是1-21。为什么呢 通常我们写代码时都是从0开始的但是这里的常量池却是从1开始因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义这种情况可用索引值0来表示。 2-常量池表 constant_pool []常量池 constant_pool是一种表结构以 1 ~ constant_pool_count - 1为索引。表明了后面有多少个常量项。常量池主要存放两大类常量字面量Literal和符号引用Symbolic References它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记用于确定该项的格式这个字节称为tag byte 标记字节、标签字节。 2.1-字面量和符号引用 在对这些常量解读前我们需要搞清楚几个概念。常量池主要存放两大类常量字面量Literal和符号引用Symbolic References。如下表 例String str xxx; 或 final int NUM 10; 2.1.1-全限定名 com/abc/test/Demo这个就是类的全限定名仅仅是把包名的.替换成/为了使连续的多个全限定名之间不产生混淆在使用时最后一般会加入一个“;”表示全限定名结束。 2.1.2-简单名称 简单名称是指没有类型和参数修饰的方法或者字段名称上面例子中的类的add()方法和num字段的简单名称分别是add和num。 2.1.3-描述符 描述符的作用是用来描述字段的数据类型、方法的参数列表包括数量、类型以及顺序和返回值。根据描述符规则基本数据类型byte、char、double、float、int、long、short、boolean以及代表无返回值的void类型都用一个大写字符来表示而对象类型则用字符L加对象的全限定名来表示详见下表: 数据类型基本数据类型 、 引用数据类型 用描述符来描述方法时按照先参数列表后返回值的顺序描述参数列表按照参数的严格顺序放在一组小括号“()”之内。如 方法java.lang.String toString()的描述符为() Ljava/lang/String; 方法int abc(int[] x, int y)的描述符为([II) I 谈谈你对符号引用、直接引用的理解 Java代码在进行Javac编译的时候并不像C和C那样有“连接”这一步骤而是在虚拟机加载Class文件的时候进行动态链接。也就是说在Class文件中不会保存各个方法、字段的最终内存布局信息因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址也就无法直接被虚拟机使用。当虚拟机运行时需要从常量池获得对应的符号引用再在类创建时或运行时解析、翻译到具体的内存地址之中。 虚拟机在加载Class文件时才会进行动态链接也就是说Class文件中不会保存各个方法和字段的最终内存布局信息因此这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时需要从常量池中获得对应的符号引用再在类加载过程中的解析阶段将其替换为直接引用并翻译到具体的内存地址中。 这里说明下符号引用和直接引用的区别与关联 符号引用符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关引用的目标并不一定已经加载到了内存中。直接引用直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用那说明引用的目标必定已经存在于内存之中了。 2.2-常量类型和结构 常量池中每一项常量都是一个表JDK1.7之后共有14种不同的表结构数据。如下表格所示 根据上图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容主要是字面量、符号引用的。比如CONSTANT_Integer_info是用来描述常量池中字面量信息的而且只是整型字面量信息。 标志为15、16、18的常量项类型是用来支持动态语言调用的jdk1.7时才加入的。 细节说明 CONSTANT_Class_info 结构用于表示类或接口CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info结构表示字段、方法和接口方法CONSTANT_String_info结构用于表示String类型的常量对象CONSTANT_Integer_info和CONSTANT_Float_info 表示4字节int和float的数值常量CONSTANT_Long_info和CONSTANT_Double_info结构表示8字节(long和double)的数值常量在class文件的常量池表中所有的8字节常量均占两个表成员项的空间。如果一个CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池表中的索引位n则常量池表中下一个可用项的索引位n2此时常量池表中索引为n1的项仍然有效但必须视为不可用的。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)的常量。 解析方式 一个字节一个字节的解析使用javap命令解析javap -verbose Demo.class 或 jclasslib工具会更方便。 总结 这14种表或者常量项结构的共同点是表开始的第一位是一个u1类型的标志位tag代表当前这个常量项使用的是哪种表结构即哪种常量类型。在常量池列表中CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。这14种常量项结构还有一个特点是其中13个常量项占用的字节固定只有CONSTANT_Utf8_info占用字节不固定其大小由length决定。为什么呢因为从常量池存放的内容可知其存放的是字面量和符号引用最终这些内容都会是一个字符串这些字符串的大小是在编写程序时才确定比如你定义一个类类名可以取长取短所以在没编译前大小不固定编译后通过utf-8编码就可以知道其长度。 访问标识
访问标识(access_flag、访问标志、访问标记)
在 常量池后紧跟着访问标记。该标记使用两个字节表示用于识别一些类或者接口层次的访问信息包括这个 Class 是类还是接口是否定义为 public 类型是否定义为 abstract 类型如果是类的话是否被声明为 final 等。各种访问标记如下所示 类的访问权限通常为 ACC_ 开头的常量。每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如若是public final的类则该标记为ACC_PUBLIC | ACC_FINAL。使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。
补充说明
1. 带有ACC_INTERFACE标志的class文件表示的是接口而不是类反之则表示的是类而不是接口。 1如果一个class文件被设置了 ACC_INTERFACE 标志那么同时也得设置ACC_ABSTRACT 标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 标志。 2如果没有设置ACC_INTERFACE标志那么这个class文件可以具有上表中除 ACC_ANNOTATION外的其他所有标志。当然ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
2. ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于Java SE 8及后续版本来说无论class文件中这个标志的实际值是什么也不管class文件的版本号是多少Java虚拟机都认为每个class文件均设置了ACC_SUPER标志。 1ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的 ACC_SUPER标志在由JDK 1.0.2之前的编译器所生成的access_flags中是没有确定含义的如果设置了该标志那么Oracle的Java虚拟机实现会将其忽略。
3. ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的而不是由源代码生成的。
4. 注解类型必须设置ACC_ANNOTATION标志。如果设置了 ACC_ANNOTATION标志 那么也必须设置ACC_INTERFACE标志。
5. ACC_ENUM标志表明该类或其父类为枚举类型。
6. 表中没有使用的access_flags标志是为未来扩充而预留的这些预留的标志在编译器中应该设置为0, Java虚拟机实现也应该忽略它们。 类索引、父类索引、接口索引集合
在访问标记后会指定该类的类别、父类类别以及实现的接口格式如下 这三项数据来确定这个类的继承关系。
类索引用于确定这个类的全限定名父类索引用于确定这个类的父类的全限定名。由于 Java语言不允许多重继承所以父类索引只有一个除了java.lang.Object 之外所有的Java类都有父类因此除了java.lang.Object 外所有Java类的父类索引都不为 0。接口索引集合就用来描述这个类实现了哪些接口这些被实现的接口将按 implements 语句如果这个类本身是一个接口则应当是 extends 语句后的接口顺序从左到右排列在接口索引集合中。
1.this_class类索引 2字节无符号整数指向常量池的索引。它提供了类的全限定名,如com/abc/java1/Demo。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体该结构体表示这个class文件所定义的类或接口。
2.super_class 父类索引 2字节无符号整数指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类其默认继承的是java/lang/Object类。同时由于Java不支持多继承所以其父类只有一个。 superclass指向的父类不能是final。
3. interfaces 指向常量池索引集合它提供了一个符号引用到所有已实现的接口 由于一个类可以实现多个接口因此需要以数组形式保存多个接口的索引表示接口的每个索引也是一个指向常量池的CONSTANT_Class (当然这里就必须是接口而不是类)。
3.1 interfaces_count (接口计数器) interfaces_count项的值表示当前类或接口的直接超接口数量。
3.2 interfaces [](接口索引集合) interfaces []中每个成员的值必须是对常量池表中某项的有效索引值它的长度为 interfaces_count。 每个成员 interfaces[i]必须为 CONSTANT_Class_info结构其中 0 i interfaces_count。在 interfaces[]中各成员所表示的接口顺序和对应的源代码中给定的接口顺序从左至右一样即 interfaces[0]对应的是源代码中最左边的接口。 字段表集合
fields
用于描述接口或类中声明的变量。字段field包括类级变量以及实例级变量但是不包括方法内部、代码块内部声明的局部变量。(local variables)字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的只能引用常量池中的常量来描述。它指向常量池索引集合它描述了每个字段的完整信息。比如字段的标识符、访问修饰符public、private或protected、是类变量还是实例变量static修饰符、是否是常量final修饰符等。
注意事项
字段表集合中不会列出从父类或者实现的接口中继承而来的字段但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性会自动添加指向外部类实例的字段。在Java语言中字段是无法重载的两个字段的数据类型、修饰符不管是否相同都必须使用不一样的名称但是对于字节码来讲如果两个字段的描述符不一致那字段重名就是合法的。 1-字段计数器 fields_count 字段计数器 fields_count的值表示当前class文件fields表的成员个数。使用两个字节来表示。fields表中每个成员都是一个field_info结构用于表示该类或接口所声明的所有类字段或者实例字段不包括方法内部声明的变量也不包括从父类或父接口继承的那些字段。 2-字段表 fields []字段表 fields表中的每个成员都必须是一个fields_info结构的数据项用于表示当前类或接口中某个字段的完整描述。一个字段的信息包括如下这些信息。这些信息中各个修饰符都是布尔值要么有要么没有。 作用域public、private、protected修饰符 是实例变量还是类变量static修饰符 可变性final 并发可见性volatile修饰符是否强制从主内存读写 可否序列化transient修饰符 字段数据类型基本数据类型、对象、数组 字段名称 字段表结构 字段表作为一个表同样有他自己的结构 字段表访问标识: 我们知道一个字段可以被各种关键字去修饰比如作用域修饰符public、private、protected、static修饰符、final修饰符、volatile修饰符等等。因此其可像类的访问标志那样使用一些标志来标记字段。字段的访问标志有如下这些 字段名索引: 根据字段名索引的值查询常量池中的指定索引项即可。 描述符索引: 描述符的作用是用来描述字段的数据类型、方法的参数列表包括数量、类型以及顺序和返回值。根据描述符规则基本数据类型byte,char,double,float,int,long,short,boolean及代表无返回值的void类型都用一个大写字符来表示而对象则用字符L加对象的全限定名来表示如下所示 属性表集合: 一个字段还可能拥有一些属性用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute_count中属性具体内容存放在attributes数组中。 以常量属性为例结构为 ConstantValue_attribute{ u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index; } 说明对于常量属性而言attribute_length值恒为2。 方法表集合
methods指向常量池索引集合它完整描述了每个方法的签名。
在字节码文件中每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private或protected),方法的返回值类型以及方法的参数信息等。如果这个方法不是抽象的或者不是native的那么字节码中会体现出来。一方面methods表只描述当前类或接口中声明的方法不包括从父类或父接口继承的方法。另一方面methods表有可能会出现由编译器自动添加的方法最典型的便是编译器产生的方法信息(比如类(接口)初始化方法clinit()和实例初始化方法init())。
使用注意事项 在Java语言中要重载(Overload)一个方法除了要与原方法具有相同的简单名称之外还要求必须拥有一个与原方法不同的特征签名特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合也就是因为返回值不会包含在特征签名之中因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中特征签名的范围更大一些只要描述符不是完全一致的两个方法就可以共存。也就是说如果两个方法有相同的名称和特征签名但返回值不同那么也是可以合法共存于同一个class文件中。 也就是说尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法但是和Java语法规范相反字节码文件中却恰恰允许存放多个方法签名相同的方法唯一的条件就是这些方法之间的返回值不能相同。 1-方法计数器 methods_count 方法计数器 methods_count的值表示当前class文件methods表的成员个数。使用两个字节来表示。methods 表中每个成员都是一个method_info结构。 2-方法表 methods []方法表 methods表中的每个成员都必须是一个method_info结构用于表示当前类或接口中某个方法的完整描述。如果某个method_info结构的access_flags项既没有设置 ACC_NATIVE 标志也没有设置ACC_ABSTRACT标志那么该结构中也应包含实现这个方法所用的Java虚拟机指令。method_info结构可以表示类和接口中定义的所有方法包括实例方法、类方法、实例初始化方法和类或接口初始化方法方法表的结构实际跟字段表是一样的方法表结构如下 方法表访问标志 跟字段表一样方法表也有访问标志而且他们的标志有部分相同部分则不同方法表的具体访问标志如下 方法名索引 根据方法名索引的值查询常量池中的指定索引项即可。 描述符索引 根据描述符索引的值查询常量池中的指定索引项即可。 属性计数器 根据属性计数器的值判断出方法中属性的个数。 属性表 属性计数器后面就是属性表了由于只有一个属性所以这里也只有一个属性表。 属性表集合
属性表集合(attributes) 方法表集合之后的属性表集合指的是class文件所携带的辅助信息比如该 class 文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行以及Java程序的调试一般无须深入了解。此外字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。 属性表集合的限制没有那么严格不再要求各个属性表具有严格的顺序并且只要不与已有的属性名重复任何人实现的编译器都可以向属性表中写入自己定义的属性信息但Java虚拟机运行时会忽略掉它不认识的属性。 1-属性计数器 attributes_count 属性计数器 attributes_count的值表示当前class文件属性表的成员个数。属性表中每一项都是一个attribute_info结构。 2-属性表 attributes []属性表 属性表的每个项的值必须是attribute_info结构。属性表的结构比较灵活各种不同的属性只要满足以下结构即可。 属性的通用格式: 即只需说明属性的名称以及占用位数的长度即可属性表具体的结构可以去自定义。 属性类型: 属性表实际上可以有很多类型上面看到的Code属性只是其中一种Java8里面定义了23种属性。 下面这些是虚拟机中预定义的属性 或查看官网 ①ConstantValue属性 ConstantValue 属性表示一个常量字段的值。位于 field_info结构的属性表中。 ConstantValue_attribute {u2 attribute_name_index;u4 attribute_length;u2 constantvalue_index;//字段值在常量池中的索引常量池在该索引处的项给出该属性表示的常量值。例如值是long型的在常量池中便是CONSTANT_Long
} ②Deprecated属性 Deprecated 属性是在 JDK 1.1 为了支持注释中的关键词deprecated 而引入的。 Deprecated_attribute {u2 attribute_name_index;u4 attribute_length;
} ③Code属性 Code属性就是存放方法体里面的代码。但是并非所有方法表都有Code属性。像接口或者抽象方法他们没有具体的方法体因此也就不会有Code属性了。 Code属性表的结构,如下图 可以看到Code属性表的前两项跟属性表是一致的即Code属性表遵循属性表的结构后面那些则是他自定义的结构。④InnerClasses属性 为了方便说明特别定义一个表示类或接口的 Class 格式为 C。如果 C 的常量池中包含某个CONSTANT_Class_info 成员且这个成员所表示的类或接口不属于任何一个包那么 C 的ClassFile 结构的属性表中就必须含有对应的 InnerClasses 属性。InnerClasses 属性是在 JDK 1.1 中为了支持内部类和内部接口而引入的,位于 ClassFile结构的属性表。⑤LineNumberTable属性 LineNumberTable 属性是可选变长属性位于 Code结构的属性表。 LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数。 start_pc,即字节码行号;line_number即Java源代码行号。 在 Code 属性的属性表中,LineNumberTable 属性可以按照任意顺序出现此外多个 LineNumberTable属性可以共同表示一个行号在源文件中表示的内容即 LineNumberTable 属性不需要与源文件的行一一对应。 LineNumberTable属性表结构: ⑥LocalVariableTable属性 LocalVariableTable 是可选变长属性位于 Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在 Code 属性的属性表中LocalVariableTable 属性可以按照任意顺序出现。 Code 属性中的每个局部变量最多只能有一个 LocalVariableTable 属性。 start pc length表示这个变量在字节码中的生命周期起始和结束的偏移位置this生命周期从头0到结尾 index就是这个变量在局部变量表中的槽位槽位可复用 name就是变量名称 Descriptor表示局部变量类型描述 LocalVariableTable 属性表结构: ⑦Signature属性 Signature 属性是可选的定长属性位于 ClassFile field_info 或 method_info结构的属性表中。在 Java 语言中任何类、 接口、 初始化方法或成员的泛型签名如果包含了类型变量 Type Variables 或参数化类型 Parameterized Types则 Signature 属性会为它记录泛型签名信息。⑧SourceFile属性 SourceFile属性结构 可以看到其长度总是固定的8个字节。⑨其他属性 Java虚拟机中预定义的属性有20多个这里就不一一介绍了通过上面几个属性的介绍只要领会其精髓其他属性的解读也是易如反掌。 小结 oracle官方的反解析工具javap
解析字节码的作用 通过反编译生成的字节码文件我们可以深入的了解java代码的工作机制。但是自己分析类文件结构太麻烦了除了使用第三方的jclasslib工具之外oracle官方也提供了工具javap。 javap是jdk自带的反解析工具。它的作用就是根据class字节码文件反解析出当前类对应的code区字节码指令、局部变量表、异常表和代码行偏移量映射表、常量池等信息。 通过局部变量表我们可以查看局部变量的作用域范围、所在槽位等信息甚至可以看到槽位复用等信息。
javac -g操作 解析字节码文件得到的信息中有些信息如局部变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等等需要在使用javac编译成class文件时指定参数才能输出。 比如你直接javac xx.java就不会在生成对应的局部变量表等信息如果你使用javac -g xx.java就可以生成所有相关信息了。如果你使用的eclipse或IDEA则默认情况下eclipse、IDEA在编译时会帮你生成局部变量表、指令和代码行偏移量映射表等信息的。
javap的用法 使用举例 总结 字节码指令集与解析概述 字节码指令