当前位置: 首页 > news >正文

泰州网站建设 思创wordpress for ipad

泰州网站建设 思创,wordpress for ipad,附近cad制图培训班,上海平台网站建设报梳理Java虚拟机相关的面试题#xff0c;主要参考《深入理解Java虚拟机 JVM高级特性与最佳实践》(第2版, 周志明 著)一书#xff0c;其余部分整合网络相关内容。注意#xff0c;关于Java并发编程的面试题因为内容较多#xff0c;单独整理。Java基础相关的面试题可以参考Java…梳理Java虚拟机相关的面试题主要参考《深入理解Java虚拟机 JVM高级特性与最佳实践》(第2版, 周志明 著)一书其余部分整合网络相关内容。注意关于Java并发编程的面试题因为内容较多单独整理。Java基础相关的面试题可以参考Java基础常见面试题总结一文。 Java程序从编译到运行的整个过程 学习Java语言需要搞清楚的第一个问题就是Java程序是如何运行起来的。 与C语言先编译成二进制文件然后直接在机器上执行不同Java语言会先编译成字节码文件.class后缀然后将字节码文件解释成二进制代码最后在机器上执行。 Java编译器将Java源文件.java文件编译成字节码文件.class文件是特殊的二进制文件二进制字节码文件这种字节码就是JVM的“机器语言”。javac.exe可以简单看成是Java编译器。 Java解释器是JVM的一部分。Java解释器用来解释执行Java编译器编译后的程序。java.exe可以简单看成是Java解释器。 说一下无关性 这里主要指Java语言的平台无关性和语言无关性。 Java语言在诞生之初就考虑到平台无关性。这里的平台是指操作系统。Java在诞生之初就提出过一个宣传口号一次编写到处运行(Write OnceRun Anywhere)。在各种不同的硬件体系结构和不同的操作系统长期并存发展的背景下平台无关性的地位日渐重要。Java虚拟机在实现平台性时使用一种平台无关的字节码从而实现了程序的一次编写到处运行。 实现语言无关性的基础是虚拟机和字节码存储格式。Java虚拟机不与包括Java在内的任何语言绑定它只与Class文件这种特定的二进制文件格式所关联。基于安全方面的考虑Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。**Class文件实现了Java虚拟机与Java语言的解耦。**Java虚拟机提供的语言无关性示例图如下 JDK和JRE JDK(Java Development Kit)是用于支持Java程序开发的最小环境。由Java语言、Java虚拟机、Java API类库三个部分组成。 JRE(Java Runtime Environment)是支持Java程序运行的标准环境。由Java SE API和Java虚拟机两部分组成。 JDK中包含JRE。 简单来说如果你需要运行 java 程序只需安装 JRE 就可以了如果你需要编写 java 程序需要安装 JDK。 JVM 体系结构 JVM 体系结构分为四部分 (1) 类加载器ClassLoader用于装载 .class 文件 (2) 执行引擎用于执行字节码或者执行本地方法 (3) 运行时数据区包括方法区、堆、Java 栈、PC 寄存器、本地方法栈 (4) 垃圾收集器用于回收废弃垃圾 类文件结构 类文件也就是Class文件是一组以8位字节为基础单位的二进制流各个数据项严格按照顺序紧凑地排列在Class文件中中间没有添加任何分隔符这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。各数据项按照“高位在前”大端模式Big-Endian的方式进行存储。 根据Java虚拟机规范Class文件格式采用一种类似于C语言结构体的伪结构体来存储这种伪结构中只有两种数据类型无符号数和表。无符号数属于基本数据类型以u1、u2、u4、u8来分别代表1、2、4、8个字节的无符号数。表是由多个无符号数或其他表作为数据项构成的符合数据类型所有的表都习惯性地以“_info”结尾。整个Class文件本质上就是一张表它由如下所示的数据项构成。 魔数Magic Number 每个class文件开头的四个字节 称为魔数。这个魔数的唯一作用是身份识别标志这个文件是否为一个能被虚拟机接受的Class文件。该魔数是一个固定值 0XCAFEBABE。 之所以没使用扩展名进行识别主要是基于安全方面的考虑文件扩展名可以随意地改动。 Class文件版本号 魔数后面的四个字节是Class文件的次版本号Minor Version占用两个字节和主版本号Major Version占用两个字节。版本号标志Class文件对应虚拟机版本。Java的版本号从45开始1.0和1.1之后的主版本向上加 1。如47表示1.3版本。 虚拟机仅Class文件做向下兼容而不支持向上兼容。也就是说高版本的JVM能识别低版本的javac编译器编译的class文件而低版本的JVM不能识别高版本的javac编译器编译的class文件。 常量池(Constant Pool) 版本号后面的是常量池。常量池容量不固定在常量池的入口使用u2类型数据记录常量池容量这个计数器被称为常量池容量计数器Constant Pool Counter。注意该值从1开始计数。保留0来表示“不引用任何一个常量池数据项”。各个数据项通过索引值访问时间复杂度为O(1)。 Class文件中常量池与运行时常量池是两个不同的概念。运行时常量池在内存中而Class文件中常量池在文件中两者的相似之处是都主要存储字面量Literal和符号引用Symbolic Reference。其中字面量主要包括字符串声明为final常量的值或者某个属性的初始值等而符号引用主要存储类和接口的全限定名称Fully Qualified Name字段的名称和描述符方法的名称和描述符。作为动态语言虚拟机在运行期会将符号引用转化成真正的内存入口地址。而JVM内存模型方法区中的运行时常量池除了存放存放编译期的字面量以及符号引用外还可以存储运行时常量。最具代表性的就是String的intern方法。 常量池支持的数据项类型如下 类型访问标志(Access Flag) 常量池后是占有两个字节的访问标志Access Flag用来识别类或接口的访问信息比如这个Class是类还是接口是public还是private是否被声明为final等。具体标志位及含义如下 两个字节的访问标志共16个标志位使用逻辑或运算对可重叠标志进行合并。 类索引This Class、父类索引Super Class和接口索引集合Interfaces 访问标志的后面是类索引、父类索引和接口索引集合。Java中是单继承多实现除Object类外每个类都有父类所以父类唯一而一个类可以实现多个接口因此接口不唯一。类索引和父类索引都是用一个u2类型数据表示而接口索引集合则是一组u2类型数据表示。 类索引和父类索引各指向一个类型为CONSTANT_Class_info的类描述符常量用来描述具体的类。接口索引第一项u2则为接口索引计数器用来记录实现了多少个接口如果为0则后面不再占用任何字节。 字段表集合(Fields) 类索引、父类索引和接口索引集合后面是字段数量fields_count 和字段表field_info集合,其中 fields_count表示类中field_info表的数量field _info描述接口或类中声明的变量。字段field包括类的实例变量和类变量但不包含从父类继承过来的字段。字段表中包含如下信息字段的作用域public、privte、protected是实例变量还是类变量static是否可变final并发可见性volatile是否强制从主内存中读写是否可被序列化transient等字段数据类型基本数据类型、对象、数组字段名称。字段表结构如下 因修饰符都是布尔值要么有要么没有所以可用标志位表示。字段表的access_flag与类的access_flag作用类似。 紧跟access_flag的是两项索引值name_index和descriptor_index。其中name_index表示的是字段的简单名称descriptor_index 表示字段和方法的描述符。 简单名称是指没有类型和参数修饰的方法或字段名称。如类中的inc()方法和m字段的简单名称分别是“inc”和“m”。 全限定名是将类全名中的“.”替换成“/”并使用“”作为最后一个类表示全限定名结束。如“org/sun/class/string”是一个全限定名。 描述符是用来描述字段的数据类型、方法的参数列表数量、类型及顺序和返回值。根据描述符的规则基本数据类型以及代表无返回值的void类型都用一个大写的字符来表示而对象类型则用字符L加对象的全限定名来描述对于数组类型每一个维度用一个前置的 “[” 字符来描述如定义个int[][]类型的二维数组记录为“[[I”。对于方法按照先参数列表后返回值的顺序描述。参数列表按照参数顺序放在“”内如方法void login()描述符为“()V”方法java.lang.String toString()的描述符为“()Ljava.lang.String”。 在descriptor_index是attribute_count和attribute_info集合存储额外信息。(指向属性表集合) 方法表集合(Methods) 方法表集合后是方法数量methods_count 和 方法表method_info集合。Class文件存储格式中对方法和字段的描述完全一致方法表的字段结构和字段表一样包括访问标志、名称索引、描述符索引、属性表集合四项。这些数据的含义非常类似在访问标志和属性表集合有所区别。 属性表集合(Attributes) 字段表和方法表中均使用到属性表attribute_info集合用于描述某些特定场景的专有信息。虚拟机Java SE 7中预定义属性已达21项。本文仅对一些常用属性做介绍。 (1) Code 属性 Code属性存储方法体中代码Java代码编译成的字节码指令。Code 是Class文件中最重要的一个属性。如果将Java程序中的信息分为代码和元数据那么Code 属性则来记录代码其他数据项用来记录元数据类、字段、方法定义等。 异常表也是Java代码的一部分。编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。 (2) Exception属性 记录方法描述时在throws关键字后面列举的异常。 (3) StackMapTable属性 该属性在JDK 1.6发布是一个复杂的属性位于Code属性的属性表中用于辅助实现“字节码验证器”。 (4) Signature属性 Signature属性在JDK 1.5发布用来记录泛型类型以弥补Java语言的伪泛型带来的不足反射时无法获取泛型信息等。 类加载机制 虚拟机的类加载机制就是将描述类的数据从Class文件加载到内存并对数据进行校验、解析和初始化最终形成可直接使用的Java类型的过程。 类从被加载到虚拟机内存中开始直到从内存中卸载为止它的整个生命周期分为七个阶段加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。其中验证、准备、解析三个部分统称为连接。七个阶段发生的顺序如下 类加载时机 虚拟机规范中没有强行约束类加载的时机。也就是说具体的虚拟机可以自定义类加载时机。但是虚拟机严格规范了“类初始化”时机。有且只有下述五种情况会对类进行初始化 (1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时会尝试进行初始化。上述四条指令对应的Java代码场景是使用new关键字实例化对象时、读取或者设置一个类的静态字段被final修饰、已在编译器把结果放入常量池的静态字段除外时、调用一个类的静态方法。 (2) 使用java.lang.reflect包的方法对类进行反射调用(如Class.forName(“com.xx.Main”))如果类没有进行过初始化则需要先触发其初始化。 (3) 当初始化一个类时如果发现其父类还未初始化则先触发父类的初始化。 (4) 虚拟机启动时用户指定的需要执行的主类包含main()方法的类先初始化。 (5) 在使用JDK 1.7版本及更高版本时如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时会尝试对这个方法句柄对应的类进行初始化。 对于以上五种场景虚拟机规范将其行为称为对一个类进行“主动引用”。其他引用类的方式均不会触发类的初始化统称为“被动引用”。被动引用常见场景如下: (1) 子类调用父类的静态字段子类不会被初始化只有父类会初始化。也就是说对于静态字段只有直接定义这个字段的类才会被初始化。 (2) 通过数组定义来引用类不会触发该类的初始化。 (3) 访问类常量(static final修饰字段)不会触发该类的初始化。 类和接口在加载阶段的区别 接口的加载与类的加载稍有不同 (1) 接口中不能使用static{}块。 (2) 当一个接口在初始化时并不要求其父接口全部都完成初始化只有真正在使用到父接口时例如引用接口中定义的常量才会初始化(延迟加载)。 类加载过程简介 虚拟机中类加载的全过程包括加载、验证、准备、解析、初始化、使用、卸载等七个阶段。 在这七个阶段中加载、验证、准备、初始化等阶段发生的顺序是确定的而解析阶段则不一定它在某些情况下会在初始化阶段之后开始这是为了支持Java语言的运行时绑定。另外上述几个阶段是按顺序开始这些阶段是互相交叉进行通常在一个阶段执行的过程中会调用或激活另一个阶段。 加载(Loading) 加载就是将类的二进制流加载到内存中。详细来说在加载阶段虚拟机需要完成以下三件事情 1通过一个类的全限定名来获取其定义的二进制字节流 2将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构 3在 Java 堆中生成一个代表这个类的java.lang.Class对象作为方法区中这个类的数据的访问入口。 注意虚拟机没有严格限制二进制字节流的获取来源。除了从Class文件中获取还可以从ZIP包中获取Jar包、EAR包、VWAR包、从网络中获取最典型的应用是Applet、运行时计算生成使用动态代理技术、其他文件生成JSP应用、从数据库中读取等。 加载阶段准确地说是加载阶段获取类的二进制字节流的动作是开发人员可控性最强的阶段开发人员既可以使用系统提供的类加载器来完成加载也可以自定义自己的类加载器来完成加载。加载阶段的核心是“类加载器”。 加载阶段完成后虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中并在Java堆中也创建一个java.lang.Class类的对象这样便可以通过该对象访问方法区中的这些数据。 验证(Verification) 验证的目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求而且不会危害虚拟机自身的安全。 虚拟机对类验证的实现大致都会包含以下四个阶段的验证文件格式验证、元数据验证、字节码验证和符号引用验证。 1文件格式验证验证字节流是否符合Class文件格式规范并且能被当前版本的虚拟机处理。该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。后面三个验证都是基于方法区的存储结构进行的。 2元数据验证对类的元数据信息进行语义分析对类中的各数据类型进行语法校验保证不存在不符合Java语法规范的元数据信息。 3字节码验证该阶段验证的主要工作是进行数据流和控制流分析确保程序语义是合法、符合逻辑的。对类的方法体进行校验分析以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。字节码校验从类型推导转变为类型检查StackMapTable属性记录是否合法节省验证所需时间。 4符号引用验证就是对对类自身以外的信息进行匹配性验证。该验证发生在虚拟机将符号引用转化为直接引用的时候该验证的主要目的是确保解析动作能正常执行。 准备(Preparation) 准备阶段是为类变量分配内存并设置类变量初始值的阶段这些内存都将在方法区中分配。对于该阶段有以下两点需要注意 1分配内存的仅包括类变量static而不包括实例变量实例变量会在对象实例化时随着对象一块分配在Java堆中。 2这里的初始值仅是数据类型默认的零值如0、0L、null、false等而不是在Java代码中被显式地赋予的值。假设一个类变量的定义为 public static int value 3那么变量value在准备阶段过后的初始值为0而不是3因为这时候尚未开始执行任何Java方法而把value赋值为3的putstatic指令是在程序编译后存放于类构造器方法之中的所以把value赋值为3的动作将在初始化阶段才会执行。 (3) 字段属性表存在ConstantValue属性类常量static final时其值会初始化为ConstantValue指定的值。 解析(Resolution) 解析是虚拟机将常量池中的符号引用转化为直接引用的过程。 符号引用和直接引用 Class文件中不会保存类或接口、字段、方法的内存布局信息虚拟机会在加载Class文件时进行动态连接将符号引用转换为直接引用也就是解析的过程。符号引用和直接引用的区别与关联 1符号引用一组符号来描述所引用的目标。符号引用与虚拟机实现的内存布局无关引用的目标无需加载到内存。 2直接引用直接指向目标的指针、相对偏移量或间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关同一个符号引用在不同虚拟机实例上解析的直接引用一般不会相同。直接引用引用的目标必定已经存在于内存中。 解析时机 解析阶段可能开始于初始化之前也可能在初始化之后开始虚拟机会根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析初始化之前还是等到一个符号引用将要被使用前才去解析它初始化之后。 解析目标 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符七类符号引用进行分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info七种常量类型。 其中方法类型、方法句柄和调用点限定符位是JDK 1.7新增与动态语言特性相关。 1类或接口的解析将非数组类型或数组类型调用不同的类加载器解析成直接引用。在解析完成前还会确认访问权限。 2字段解析对字段解析前会获取字段所属类或接口的符号引用如果没有则触发该类或接口的解析。获得字段所属类或接口的符号引用后先在该类或接口中查找匹配的字段。如果没有则会按照继承关系从上往下递归搜索该类所实现的各个接口和其父接口。如果还没有则按照继承关系从上往下递归搜索其父类直至查找结束。在解析完成前同样也会确认访问权限。 3类方法解析对类方法的解析与对字段解析的搜索步骤相似只是多了判断该方法所处的是类还是接口的步骤而且对类方法的匹配搜索是先搜索父类再搜索接口。在解析完成前同样也会确认访问权限。 4接口方法解析与类方法解析步骤类似接口不会有父类因此只会递归向上搜索父接口。由于接口方法默认是public所以不存在访问权限问题。 初始化(Initialization) 特指类的初始化。初始化是执行类构造器()方法的过程。相比准备阶段变量设置系统要求的初始值初始化阶段会根据程序员指定的主观计划去初始化类变量和其他资源。 ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。 ()方法不同于构造方法类或实例的()方法。在子类的调用前保证父类的()方法已被调用。 ()方法是线程安全的执行的线程需先获取锁才能进行初始化操作保证只有一个线程能执行(利用此特性可以实现线程安全的懒汉单例模式)。 使用 当类完成初始化后就可以使用该类了。 卸载 当用户程序代码执行完毕后JVM 便开始销毁创建的 Class 对象最后负责运行的 JVM 也退出内存。注意这里销毁的是类对象不是实例对象与垃圾回收无关。 类加载器 类加载器是根据一个类的全限定名来获取描述此类的二进制字节流。该功能模块被放到Java虚拟机外部实现实现了应用程序自定义所需类的获取方式。 类和类加载器 对于任意一个类都需要由加载它的类加载器和这个类本身一同确立其唯一性每个类加载器都有一个独立的类名称空间。 也就是说同一个类在多个类加载器加载后不再相等。 双亲委派模型Parents Delegation Model Java 中类加载器根据是否继承自java.lang.ClassLoader类可分成两类一类是启动类加载器Bootstrap ClassLoader另一其他类加载器Other ClassLoader。启动类加载器使用C实现由虚拟机提供。其他类加载器由Java语言实现独立于虚拟机外部。是由 Java 应用开发人员编写的。 从Java开发人员的角度类加载器可细为四类引导类加载器Bootstrap ClassLoader、扩展类加载器Extension ClassLoader、应用程序类加载器Application ClassLoader、自定义类加载器User ClassLoader。 (1) 引导类加载器Bootstrap ClassLoader 用来加载 Java 的核心库放在JAVA_HOME\lib目录或-Xbootclasspath参数指定目录且虚拟机指定的文件名。该类加载器不继承 java.lang.ClassLoader如需将加载请求委派给引导类加载直接使用null即可。 (2) 扩展类加载器Extension ClassLoader 用来加载 Java 的扩展库放在JAVA_HOME\lib\ext目录或java.ext.dirs 系统变量指定的路径。该类加载器继承自 java.lang.ClassLoader如需将加载请求委派给引导类加载可直接调用sun.misc.Launcher E x t C l a s s L o a d e r 。 ( 3 ) 应用程序类加载器 A p p l i c a t i o n C l a s s L o a d e r 用来加载用户类库放在用户类路径 C L A S S P A T H 。由于这个类加载器是 C l a s s L o a d e r 中的 g e t S y s t e m C l a s s L o a d e r ( ) 方法的返回值所以与将其称为“系统类加载器 S y s t e m C l a s s L o a d e r ”。该类加载器继承自 j a v a . l a n g . C l a s s L o a d e r 如需将加载请求委派给引导类加载可直接调用 s u n . m i s c . L a u n c h e r ExtClassLoader。 (3) 应用程序类加载器Application ClassLoader 用来加载用户类库放在用户类路径CLASSPATH。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值所以与将其称为“系统类加载器System ClassLoader”。该类加载器继承自 java.lang.ClassLoader如需将加载请求委派给引导类加载可直接调用sun.misc.Launcher ExtClassLoader。(3)应用程序类加载器ApplicationClassLoader用来加载用户类库放在用户类路径CLASSPATH。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值所以与将其称为“系统类加载器SystemClassLoader”。该类加载器继承自java.lang.ClassLoader如需将加载请求委派给引导类加载可直接调用sun.misc.LauncherApplicationClassLoader。 (4) 用户类加载器User ClassLoader。JVM建议用户将应用程序类加载器作为自定义类加载器的父类加载器。 双亲委派模型简介 类加载器之间的层次关系称为双亲委派模型。 双亲委派模型在JDK 1.2被引入。这里的“双亲”不是指父母而更倾向于父类指代当前类加载器的基类加载器。Java Doc 对其过程描述是 The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a parent class loader. When loading a class, a class loader first delegates the search for the class to its parent class loader before attempting to find the class itself. 双亲委派模型工作过程是 如果一个类加载器收到了类加载请求它并不会自己先去加载这个类而是把这个请求委托给父类的加载器去执行每一层的类加载器都进行该操作因此请求最终将到达顶层的启动类加载器。只有当父类加载器无法完成此加载任务子加载器才会尝试自己去加载。 双亲委派模型优势 双亲委派模型的优势是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。这种机制的好处是 1首先这种层级关系可以避免类的重复加载当父亲已经加载了该类时就没有必要在子ClassLoader再加载一次。 2其次是考虑到安全因素。Java 核心 API 中定义类型不应被随意替换。假设通过网络传递一个名为java.lang.Integer的类通过双亲委托模式传递到启动类加载器而启动类加载器在核心Java API发现这个名字的类发现该类已被加载并不会重新加载网络传递的过来的java.lang.Integer而直接返回已加载过的Integer.class这样便可以防止核心API库被随意篡改。同时java.lang作为核心API包需要访问权限强制定义一个启动类加载器无法解析的类后在自定义类加载中不会被加载。 双亲委派模型的实现如下 protected synchronized Class? loadClass(String name, boolean resolve) throws ClassNotFoundException {Class currentClass findLoadedClass(name); // 首先判断是否已经加载过该类if(currentClass null) {try {if(parent ! null) {currentClass parent.loadClass(name, resolve);} else {currentClass findBoostrapClassOrNull(name);}}catch(ClassNotFoundException exception) {// 父类无法完成加载请求}if(currentClass null) {c findClass(name);// 调用自身findClass方法加载类}}if(resolve) {resolveClass(currentClass);} }破坏双亲委派模型 任何事物都具有两面性双亲委派模型也不例外。在双亲委派模型的发展中有三次较大的“被破坏”情况。 向下兼容 双亲委派模型的第一次“被破坏”发生在双亲委派模型出现之前。 双亲委派模型发布于JDK 1.2所以JDK 1.2之前的版本JDK 1.0 和 JDK 1.1实现的类加载器不支持双亲委派模型。在JDK 1.2之前用户通过继承java.lang.ClassLoader并重写loadClass()方法实现自定义类加载器。比如web容器JBoss、Tomcat、Weblogic实现了自定义类加载器。 为支持向后兼容JDK1.2之后的java.lang.ClassLoader添加一个新的proceted方法findClass()。JDK1.2之后已不再提倡用户去覆盖loadClass()方法而是把自己的类加载逻辑写到findClass()方法中。而对于基于重写loadClass()方法的方式如果父类加载器加载失败则可调用自己的findClass()方法来完成加载这样就可以保证新写出来的类加载器是符合双亲委派模型的。 实现双亲委派模型的两种方式 方法一 重写父类java.lang.ClassLoader的findClass()方法。该方法仅适用于JDK 1.2及之后版本的类加载器。 方法二 重写父类java.lang.ClassLoader的loadClass()方法和findClass()方法并在调用loadClass()方法尝试使用父类加载器加载失败后调用自己的findClass()方法。 支持SPI调用 双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的。 双亲委派模型解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载)但是却无法解决基础类调用用户代码的场景。一个典型的例子便是JNDI服务它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar中)但JNDI的目的就是对资源进行集中管理和查找它需要调用独立厂商实现部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码但启动类加载器不可能“认识”这些代码。 为了解决这个困境Java设计团队只好引入了一个不太优雅的设计线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置如果创建线程时还未设置它将会从父线程中继承一个如果在应用程序的全局范围内都没有设置过那么这个类加载器默认就是应用程序类加载器。 这样JNDI服务就可通过线程上下文类加载器去加载所需SPI代码也就是父类加载器请求子类加载器去完成类加载动作这种行为实际上打通了双亲委派模型的层次结构来逆向使用类加载器已经违背双亲委派模型。Java中所有涉及SPI的加载动作基本上都采用这种方式例如JNDI,JDBC,JCE,JAXB和JBI等。 支持热部署 双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的。 用户希望应用程序能像计算机外设一样不用重启机器就能立即使用。对一些生产系统“热部署”代码热替换、模块热部署尤其是企业级软件开发者具有极大的吸引力。 模块化规范化之争主要有Sun主导的Jigsaw项目和已经成为业界“事实上”的Java模块化标准的OSGi。 OSGi的原理是每个程序模块OSGi称之为Bundle都有一个自己的类加载器当需要更换一个Bundle时将Bundle连同类加载器一起换掉程序模块和类加载器绑定在一起以实现代码的热替换。 在OSGi环境下类加载器不再是双亲委派模型中的树状结构而是进一步发展为网状结构。 字节码指令简介 字节码指令存储在Code属性中。每个字节码指令是一个字节长度(0~255)的操作码Opcode和零到多个操作数Operands也就是说指令集的操作码个数不能超过256个。 指令集中大多数的指令都包含其操作所对应的数据类型信息。如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中。 但由于虚拟机操作码长度只有一个字节所以包含数据类型的操作码为指令集的设计带来压力如果每一种数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话那指令集的数据就会超过256个了。因此虚拟机只提供有限的指令集来支持所有的数据类型。 如load 操作 只有iload、lload、fload、dload、aload用来支持int、long、float、double、reference 类型的入栈而对于boolean 、byte、short 和char 则没有专门的指令来进行运算。编译器会在编译期或运行期将byte 和 short 类型的数据带符号扩展为int类型的数据将boolean 和 char 类型的数据零位扩展为相应的int 类型数据。 加载和存储指令 加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输。 1)将一个局部变量加载到操作数栈的指令包括iload,iload_lload、lload_、float、 fload_、dload、dload_aload、aload_。 2)将一个数值从操作数栈存储到局部变量表的指令istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_ 3)将常量加载到操作数栈的指令bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_ 4)局部变量表的访问索引指令:wide 一部分以尖括号结尾的指令代表了一组指令如iload_代表了iload_0,iload_1等这几组指令都是带有一个操作数的通用指令。 运算指令 算术指令用于对两个操作数栈上的值进行某种特定运算并把结果重新存入到操作栈顶。 1)加法指令:iadd,ladd,fadd,dadd 2)减法指令:isub,lsub,fsub,dsub 3)乘法指令:imul,lmul,fmul,dmul 4)除法指令:idiv,ldiv,fdiv,ddiv 5)求余指令:irem,lrem,frem,drem 6)取反指令:ineg,leng,fneg,dneg 7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr 8)按位或指令:ior,lor 9)按位与指令:iand,land 10)按位异或指令:ixor,lxor 11)局部变量自增指令:iinc 12)比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp Java虚拟机没有明确规定整型数据溢出的情况但规定了处理整型数据时只有除法和求余指令出现除数为0时会导致虚拟机抛出异常。 Java虚拟机要求在浮点数运算的时候所有结果否必须舍入到适当的精度如果有两种可表示的形式与该值一样会优先选择最低有效位为零的。称之为最接近数舍入模式。 浮点数向整数转换的时候Java虚拟机使用IEEE 754标准中的向零舍入模式这种模式舍入的结果会导致数字被截断所有小数部分的有效字节会被丢掉。 类型转换指令 类型转换指令将两种Java虚拟机数值类型相互转换这些操作一般用于实现用户代码的显式类型转换操作。 JVM直接就支持宽化类型转换(小范围类型向大范围类型转换) 1)int类型到long,float,double类型 2)long类型到float,double类型 3)float到double类型 但在处理窄化类型转换时必须显式使用转换指令来完成这些指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。 将int 或 long 窄化为整型T的时候仅仅简单的把除了低位的N个字节以外的内容丢弃N是T的长度。这有可能导致转换结果与输入值有不同的正负号。 在将一个浮点值窄化为整数类型T仅限于 int 和 long 类型将遵循以下转换规则 1如果浮点值是NaN 呐转换结果就是int 或 long 类型的0 2如果浮点值不是无穷大浮点值使用IEEE 754 的向零舍入模式取整获得整数v 如果v在T表示范围之内那就是v 3否则根据v的符号 转换为T 所能表示的最大或者最小正数 对象创建与访问指令 虽然类实例和数组都是对象Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。 1)创建实例的指令:new 2)创建数组的指令:newarray,anewarray,multianewarray 3)访问字段指令:getfield,putfield,getstatic,putstatic 4)把数组元素加载到操作数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload 5)将操作数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore 6)取数组长度指令:arraylength JVM支持方法级同步和方法内部一段指令序列同步这两种都是通过moniter实现的。 7)检查实例类型指令:instanceof,checkcast 操作数栈管理指令 如同操作一个普通数据结构中的堆栈那样Java 虚拟机提供了一些用于直接操作操作数栈的指令包括 1将操作数栈的栈顶一个或两个元素出栈pop、pop2 2复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 3将栈最顶端的两个数值互换swap 控制转移指令 让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括 条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等复合条件分支:tableswitch,lookupswitch无条件分支:goto,goto_w,jsr,jsr_w,ret JVM中有专门的指令集处理int和reference类型的条件分支比较操作为了可以无明显标示一个实体值是否是null,有专门的指令检测null 值。boolean类型和byte类型,char类型和short类型的条件分支比较操作都使用int类型的比较指令完成而 long,float,double条件分支比较操作由相应类型的比较运算指令运算指令会返回一个整型值到操作数栈中随后再执行int类型的条件比较操作完成整个分支跳转。各种类型的比较都最终会转化为int类型的比较操作。 方法调用和返回指令 invokevirtual指令:调用对象的实例方法根据对象的实际类型进行分派(虚拟机分派)。 invokeinterface指令:调用接口方法在运行时搜索一个实现这个接口方法的对象找出合适的方法进行调用。 invokespecial:调用需要特殊处理的实例方法包括实例初始化方法私有方法和父类方法 invokestatic:调用类方法(static) 方法返回指令是根据返回值的类型区分的包括ireturn(返回值是boolean,byte,char,short和 int),lreturn,freturn,drturn和areturn另外一个return供void方法实例初始化方法类和接口的类初始化i方法使用。 异常处理指令 在Java程序中显式抛出异常的操作throw语句都有athrow 指令来实现除了用throw 语句显示抛出异常情况外Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。 在Java虚拟机中处理异常不是由字节码指令来实现的而是采用异常表来完成的。 同步指令 方法级的同步是隐式的无需通过字节码指令来控制它实现在方法调用和返回操作中。虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是否是同步方法。方法调用时调用指令会检查该标志是否被设置若设置执行线程持有moniter然后执行方法最后完成方法时释放moniter。 同步一段指令集序列通常由synchronized块标示JVM指令集中有monitorenter和monitorexit来支持synchronized语义。 结构化锁定是指方法调用期间每一个monitor退出都与前面monitor进入相匹配的情形。JVM通过以下两条规则来保证结结构化锁成立(T代表一线程M代表一个monitor) (1) T在方法执行时持有M的次数必须与T在方法完成时释放的M次数相等 (2) 任何时刻都不会出现T释放M的次数比T持有M的次数多的情况 字节码执行引擎 JVM实现了可执行代码与操作系统的隔离。开发者在JVM上执行的代码从操作系统的机器码转变为虚拟机的字节码。而且虚拟机的执行引擎可定制指令集与执行引擎的结构体系丰富了指令集。 字节码执行引擎的概念模型统一实现如下功能将输入的字节码文件进行字节码解析后输出执行结果。 运行时栈帧结构 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储方法的局部变量表操作数栈动态连接和方法返回地址等信息。方法从调用开始到执行完成就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 栈帧运行时所需内存固定仅因具体虚拟机的实现而不同。栈帧运行时需要使用的内存已在编译代码阶段对局部变量表的size操作数栈的深度进行统计并写入到方法表的Code属性中。 对于执行引擎来讲在活动线程中只有虚拟机栈顶的栈帧才是有效的称为当前栈帧(Current Stack Frame)这个栈帧所关联的方法称为当前方法(Current Method)。 概念模型中典型的栈帧结构如下图 局部变量表(Local Variable Table) 局部变量表是一组变量值存储空间用于存放方法参数和方法内部定义的局部变量。Class文件中方法的Code属性的max_locals数据项中存储该方法所需要分配的局部变量表的最大容量。 局部变量表的容量以变量槽Variable Slot为最小单位虚拟机规范没有明确指明一个Slot应占用的内存空间大小只是约束每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。也就是说Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。如果在64位虚拟机中使用64位的物理内存空间去实现一个Slot那么虚拟机需要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。 一个Slot可以存放一个32位以内(不是32位随处理器、操作系统或虚拟机不同而变化)的数据类型。 Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress 8种类型。 reference类型表示对一个对象实例的引用。虚拟机规范既没有规定reference类型的长度也没有明确指出这种引用的结构。一般来说虚拟机实现至少都应当能通过这个引用做到两点一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息否则无法实现Java语言规范中定义的语法约束。C语言默认情况下不开启RTTI支持的情况就只能满足第一点而不满足第二点。这也是为何C中提供反射的根本原因。 returnAddress类型很少用是为字节码指令jsr、jsr_w和ret服务的。该类型指向一条字节码指令的地址很古老的Java虚拟机曾经使用这几条指令来实现异常处理现已经由异常表代替。 对于64位的数据类型虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。Java语言中明确规定reference类型则可能是32位也可能是64位64位的数据类型只有long和double两种。值得一提的是这里把long和double数据类型分割存储的做法与“long和double的非原子性协定”中把一次long和double数据类型读写分割为两次32位读写的做法有些类似。不过由于局部变量表建立在线程的堆栈上是线程私有的数据无论读写两个连续的Slot是否为原子操作都不会引起数据安全问题不存在线程安全问题。 虚拟机通过索引的方式使用局部变量表时间复杂度是O(1)索引值的范围是从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量索引n就代表了使用第n个Slot如果是64位数据类型的变量则说明会同时使用n和n1两个Slot。虚拟机规定对于存放一个64位数据的两个Slot不允许采用任何方式单独访问其中的某一个否则将抛出异常。 在方法执行时虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的如果执行的是实例方法非static的方法那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列占用从1开始的局部变量Slot参数分配完毕后再根据方法体内部定义的变量顺序和作用域分配其余的Slot。 Slot复用策略。为尽可能节省栈帧空间局部变量表中的Slot支持重用。方法体中定义的变量其作用域并不一定会覆盖整个方法体如果当前字节码PC计数器的值已经超出了某个变量的作用域那这个变量对应的Slot就可以交给其他变量使用。 Slot复用策略设计尽管可节省栈帧空间但也会伴随一些额外的副作用例如在某些情况下Slot的复用会直接影响到系统的垃圾收集行为。示例如下 public static void mainString[]args{byte[] placeholder new byte[64*1024*1024]System.gc }设置作用域后仍不会回收 public static void mainString[]args{{byte[] placeholder new byte[64*1024*1024]}System.gc }按照正常的理解placeholder在执行GC时已不再访问该部分内存理应被回收但是由于支持Slot重用策略且placeholder占用的Slot没有被其他变量复用所以作为GC Roots一部分的局部变量表仍保存对它的关联。 所以在Java编码规范中常有不使用的对象应手动赋值为null编码规则。但是这个操作真的很有必要吗《深入理解Java虚拟机》一书给出两点理由 1变量设置为null会被编译器优化掉。在使用编译器编译时会进行编译优化。对于在变量使用完毕后将其设置为null的代码会因编译器的优化策略不同可能会被优化掉。 2通过局部变量作用域来控制局部变量的回收时间时最优雅的解决方案。仅仅通过代码规范来规避Slot复用策略的缺点不是一种有效的解决方法。示例如下 public static void mainString[]args{{byte[] placeholder new byte[64*1024*1024]}int a 0;System.gc }未设置初值的局部变量不能被使用。未分配内存空间 示例代码 public static void mainString[]args{int a;System.out.println(a)// 抛出异常 }操作数栈Operand Stack 操作数栈也常称为操作栈。同局部变量表一样操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型包括long和double其中32位数据类型所占的栈容量为164位数据类型所占的栈容量为2。 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。在编译程序代码的时候编译器要严格保证这一点在类校验阶段的数据流分析中还要再次验证这一点。 栈帧重叠优化。在概念模型中两个栈帧作为虚拟机栈的元素是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起这样在进行方法调用时就可以共用一部分数据无须进行额外的参数复制传递。这主要参照局部性原理。图示如下 Java虚拟机的解释执行引擎称为“基于栈的执行引擎”其中所指的“栈”就是操作数栈。 动态连接Dynamic Linking 动态连接就是将方法的符号引用替换为运行时常量池中该栈帧所属方法的直接引用的过程。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用符号引用字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用这部分称为动态解析。 方法返回地址 当一个方法开始执行后只有两种方式可以退出这个方法正常完成出口Normal Method Invocation Completion和异常完成出口Abrupt Method Invocation Completion。 正常完成出口是通过方法返回的字节码指令实现。执行引擎在遇到return无返回值、ireturn返回值是boolean、byte、char、short和int、lreturn返回值是long、freturn返回值是float、dreturn返回值是double、areturn返回值是reference类型。 异常完成出口在方法执行过程中遇到异常且该异常没有在方法体内得到处理的场景。新的异常处理使用方法表实现。当Java虚拟机内部产生异常或代码中使用athrow字节码指令产生的异常后如果在该方法的异常表中没有搜索到匹配的异常处理器就会导致方法退出。古老的虚拟机使用jsr、jsr_w和ret字节码指令配合returnAddress类型存储一条字节码指令的地址实现异常处理。方法异常退出后不会产生任何返回值。 无论是正常完成还是异常完成方法在退出后都需要返回到方法被调用的位置程序才能继续执行。一般来说方法正常退出时调用者的PC计数器的值可以作为返回地址栈帧中很可能会保存这个计数器值。而方法异常退出时返回地址是要通过异常处理器表来确定的栈帧中一般不会保存这部分信息。 方法退出的过程实际上就等同于把当前栈帧出栈因此退出时可能执行的操作有恢复上层方法的局部变量表和操作数栈把返回值如果有的话压入调用者栈帧的操作数栈中调整PC计数器的值以指向方法调用指令后面的一条指令等。 附加信息 虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中例如与调试相关的信息这部分信息完全取决于具体的虚拟机实现。在实际开发中一般会把动态连接、方法返回地址与其他附加信息全部归为一类称为栈帧信息。 方法调用 在本文中方法调用不等同于方法执行方法调用阶段唯一的任务是确定被调用方法的版本调用哪一个方法还不涉及方法内部的具体运作过程。 方法调用的过程就是Class文件存储的符号引用替换为目标方法的直接引用。 解析调用Resolution Invoke 在类加载的解析阶段会将一部分符号引用转为直接引用也就是在编译阶段就能够确定唯一的目标方法这类方法的调用称为解析调用。 此类方法的特点是“编译期可知运行期不可变”。主要包括静态方法和私有方法两大类前者与类型直接关联后者在外部不可访问因此他们都不可能通过继承或者别的方式重写该方法。符合这两类的方法主要有以下四种静态方法、私有方法、实例构造器、父类方法。 虚拟机中提供以下五条方法调用指令 invokestatic调用静态方法解析阶段确定唯一方法版本 invokespecial调用实例构造器init方法、私有及父类方法解析阶段确定唯一方法版本 invokevirtual调用所有虚方法和final方法 invokeinterface调用接口方法 invokedynamic动态解析出需要调用的方法然后执行 前四条指令固化在虚拟机内部方法的调用执行不可人为干预而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法静态方法、私有方法、实例构造器、父类方法称为非虚方法其余的final修饰的除外称为虚方法。 对于final修饰的方法尽管使用invokeVirtual指令调用但是这这种方法无法被覆盖所以无须对方法接收者进行多态选择又或者说多态选择的结果唯一。Java语言规范明确规定final方法是一种非虚方法。 分派调用Dispatch Invoke 解析调用和分派调用是描述方法调用的不同角度本质上还是将符号引用替换成直接引用。 解析调用是一个静态过程在编译期可完全确定需要执行的方法不会在运行期处理。 分派与多态特性有关能揭示“重载”和“重写”的实现原理。 分派调用根据在编译期完成还是运行期完成可分为静态分派和动态分派。另外根据分派依据的宗量数可分为单分派和多分派。两两组合后分派可分为四类静态单分派、静态多分派、动态单分派、动态多分派。 静态分派 在学习静态分派前需要了解两个概念静态类型Static Type也称外观类型和实际类型Actual Type不是动态类型。 静态类型可以理解为变量声明的类型而实际类型就是创建这个对象的类型。如下面的man这个变量它的静态类型就是Humanman这个变量的实际类型就是Man。 Human man new Man();静态类型和实际类型的区别是静态类型的变化仅仅在使用时发生变量本身的静态类型不会发生变化并且最终的静态类型是编译期间可知的。而实际类型的变化结果在运行期才可以确定编译器在编译程序时并不知道一个对象的实际类型是什么。比如下面的代码 // 实际类型变化 Human man new Man(); man new Woman(); // 静态类型变化 sr.sayHello((Man)man); sr.sayHello((Woman)man);所有依赖静态类型来定位方法版本的分派动作叫做静态分派静态分派的典型应用是方法重载。静态分派发生在编译期间因此确定静态分派的动作实际上不是由虚拟机来执行的。 另外编译器虽然能确定出方法的重载版本但在很多情况下这个重载版本并不是唯一的往往只是一个相对来说更加合适的版本。这主要是因为字面量没有显式的静态类型它的静态类型只能通过语言上的规则去推断。而现有推断规则是一种模糊规则类型转换规则。 注意解析调用和分派调用不是二选一的关系。比如静态方法的重载是分派调用但也是解析调用。 动态分派 如果说静态分派和重载有关那么动态分派则和重写有关。 在运行期根据实际类型确定方法执行版本的分派过程叫做动态分派。动态分派的典型应用场景就是方法重写。 invokevirtual指令在多态查找时其运行时解析过程大致分为以下几个步骤 1找到操作数栈顶的第一个元素所指向的对象的实际类型记为C 2如果在类型C中找到与常量中的描述符和简单名称一样的方法则进行访问权限校验如果通过则返回这个方法的直接引用查找过程结束如果不通过返回java.lang.IllegalAccessError异常 3否则按照继承关系从下到上依次对C的各个父类进行搜索和验证 4如果还没有找到合适的方法抛出java.lang.AbstractMethodError异常。 invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型然后将常量池中的类方法符号引用解析到直接引用上这个过程就是Java语言中方法重写的本质。 动态分派示例代码如下 Human mannew Man(); Human womannew Woman(); man.sayHello(); woman.sayHello(); mannew Woman(); man.sayHello();单分派和多分派 方法的接收者与方法的参数统称为方法的宗量。根据分派基于宗量的个数可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择多分派则是基于多个宗量。 示例代码如下 public class Dispatch {static class Pepsi {}static class Coca {}public static class Father {public void like(Pepsi p) {System.out.println(Father likes pepsi);}public void like(Coca c) {System.out.println(Father likes coca);}}public static class Son extends Father {public void like(Pepsi p){System.out.println(Son likes pepsi);}public void like(Coca c) {System.out.println(Son likes coca);}}public static void main(String[] args) {Father father new Father();Father son new Son();father.like(new Coca()); // 静态多分派son.like(new Pepsi()); // 动态单分派} }在编译期father实例是静态类型是静态分派。在选择目标方法时依据两点(1)静态类型是Father还是Son; (2)方法参数是Pepsi还是Coca。这次选择产生了两个invokevirtual指令两条指令的参数分别为常量池中指向Father.like(Coca)和Father.like(Pepsi)方法的符号引用。因为是根据两个宗量进行选择所以Java语言的静态分派属于多分派类型。 在运行时son实例需要明确实际类型是动态分派过程。在执行son.like(new Pepsi())时由于编译期已经明确目标方法的签名必须是like(Pepsi)所以虚拟机此时不关心传递的参数是什么因为这时参数的静态类型、实际类型都对方法的选择不会构成影响唯一有影响的就是方法接收者的实际类型是Father还是Son。因为只有一个宗量所以Java的动态分派属于单分派。 目前Java语言是一门静态多分派、动态单分派的语言。而Java虚拟机已经提供对动态语言的支持即JDK 1.7中新增invokedynamic指令。 动态分派的实现简介 动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法基于性能的考虑大部分虚拟机都会对其进行优化。常用的“稳定优化”手段是使用“虚方法表”。也就是在方法区中建立一个虚方法表Virtual Method Table在invokeinterface执行时也会用到接口方法表Interface Method Table使用虚方法表索引来替代元数据查找以提升性能。 虚方法表存放各个方法的实际入口地址。如果某个方法在子类中没有被重写那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的都指向父类的实现入口。如果子类重写了父类的方法子类方法表中的地址会替换为指向子类实现版本的入口地址。 为了程序实现上的方便具有相同签名的方法在父类和子类的虚方法表中都应该具有一样的索引号这样当类型变换时仅仅需要变更查找的方法表就可以从不同的虚方法表中按索引转换出所需的入口地址。 方法表一般在类加载的连接阶段进行初始化完成类的变量初始值后虚拟机会把该类的方法表也初始化完毕。 动态类型语言 动态类型语言和静态类型语言 静态类型语言是在编译期进行类型检查过程的语言如C、C、Java。强类型语言。 动态类型语言是在运行期进行类型检查过程的语言如JavaScript、Lua、PHP、Ruby等。弱类型语言。 Java虚拟机的愿景是可支持多种语言运行在虚拟机上因此在虚拟机层面提供动态类型的支持很有必要。这也是JDK 1.7中invokedynamic指令及java.lang.invoke包出现的技术背景。 本文介绍的动态类型支持概念重点介绍函数指针场景。 java.lang.invoke java.lang.invoke包的重要目的除了依靠符号引用来确定调用目标方法以外还提供一种新的动态确定目标方法的机制叫做MethodHandle。 java语言无法将一个函数作为参数进行传递普遍的做法是设计一个接口然后以实现了这个接口的对象作为参数。引入java.lang.invoke后可以基于MethoedHandle实现类似功能。 public class Ser {interface IPrintable{println();}pulic static void println(IPrintable printer) {printer.println();}static class ClassA implements IPrintable{public void println(String s) {System.out.println(s);}}public static void main(String... strings) throws Throwable {println(new ClassA()); // 使用接口实现钩子Object obj System.currentTimeMillis() % 2 0 ? System.out : new ClassA();getPrintlnMH(obj).invokeExact(cadasdasda); // 使用MethodHandle实现类似函数指针功能}private static MethodHandle getPrintlnMH( Object obj) throws Throwable {// MethodType也称为方法模板用于记录方法类型第一个参数是方法返回值其余参数是方法参数MethodType mt MethodType.methodType(void.class, String.class);// lookup()方法是在指定类中查找符合方法名称、方法类型、调用权限的方法的句柄// findVirtual()方法用于查找特定类中指定方法简单名称和模板信息的方法// bindTo()方法关联方法和方法接收者return MethodHandles.lookup().findVirtual(obj.getClass(), println,mt).bindTo(obj);} }Reflection和MethodHandle 除了使用MethodHandle模拟方法调用还可以使用反射实现两者侧重定不同。 1尽管Relection和MethodHandle都是模拟方法调用但Reflection是在模拟Java代码层次的方法调用而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.lookup中的3个方法-findStaitc、findVirtual、findSpecial对应invokestatic、invokevirtual和invokespecial这几条字节码指令而这些底层细节在使用Reflection API时是不需要关心的。 2Reflection中的java.lang.reflect.Method对象远比MethodHandle机制信息含量多换句话说Reflection是重量级而MethodHandle是轻量级。 3MethodHandle性能可在虚拟机层面进行优化而Reflection则不能。 4Reflection API仅面向Java语言服务而MethodHandle则设计成面向运行在Java虚拟机之上的语言。在其他语言中引用MethodHandle对应的包 invokedynamic指令 invokedynamic指令需要与MethodHandle机制一样都是为了解决原有四条“invoke”指令方法分派规则固化在虚拟机之中后无法实现类似函数指针功能的问题从而将查找目标方法的决定权从虚拟机转移到用户代码。 每个出现的invokedynamic指令的位置都称为一个动态调用点dynamic call site。每个动态调用点在初始化时都处于未链接的状态不包含要调用方法。invokedynamic指令的第一个参数不再是代表方法符号引用等CONSTANT_Methodref_info常量而是JDK 1.7新增的CONSTANT_InvokedDynamic_info常量。这个常量包含三部分信息引导方法bootstrap method、方法类型MethodType、方法名称。 虚拟机执行invokedynamic指令时首先需要链接其对应的动态调用点。在链接的时候Java虚拟机会先调用对应的引导方法bootstrap method。这个启动方法的返回值是java.lang.invoke.CallSite类的对象。这个CallSite对象的getTarget方法可以获取到实际要调用的目标方法句柄。有了方法句柄之后对这个动态调用点的调用实际上是代理给方法句柄来完成的。也就是说对invokedynamic指令的调用实际上就等价于对方法句柄的调用具体来说是被转换成对方法句柄的invoke方法的调用。Java 7中提供了三种类型的动态调用点CallSite的实现分别是java.lang.invoke.ConstantCallSite、java.lang. invoke.MutableCallSite和java.lang.invoke.VolatileCallSite。这些CallSite实现的不同之处在于所对应的目标方法句柄的特性不同。ConstantCallSite所表示的调用点绑定的是一个固定的方法句柄一旦链接之后就无法修改MutableCallSite所表示的调用点则允许在运行时动态修改其目标方法句柄即可以重新链接到新的方法句柄上而VolatileCallSite的作用与MutableCallSite类似不同的是它适用于多线程情况用来保证对于目标方法句柄所做的修改能够被其他线程看到。这也是名称中volatile的含义所在类似于Java中的volatile关键词的作用。 invokedynamic指令面向字节码。由于传统的Java编译器并不会生成invokedynamic指令所以为利用invokedynamic指令还需要开发人员自己生成包含这个指令的字节代码。一种推荐的方式是使用第三方工具实现这个需求而不是手动修改生成的字节码如INDY工具。 方法执行 方法执行就是虚拟机执行方法中的字节码指令的过程。 执行引擎在执行Java代码时有两种模式选择解释执行通过解释器执行和编译执行通过即使编译器产生本地代码执行。 解释执行 解释执行就是执行程序时再将中间码例如Java的字节码通过JVM解释成机器码一行行的解释成机器码进行执行。这个运行过程是解释一行执行一行。 Java语言经常被人们定位为“解释执行”的语言在JDK 1.0时代这种定义还算比较准确但当主流的虚拟机都包含了即时编译器后Class文件到底是被解释执行还是编译执行就成了只有虚拟机才能准确判断的事情了。所以在当前阶段笼统地说“解释执行”对Java语言来说是没有任何意义的。只有确定了Java实现版本和执行引擎运行模式时谈论解释执行还是编译执行才会比较确切。 编译执行 编译执行就是将一段程序直接翻译成机器码(对于C/C这种非跨平台的语言)。编译执行是直接将所有语句都编译成了机器语言并且保存成可执行的机器码。执行的时候是直接进行执行机器语言不需要再进行解释/编译。 内存管理 C、 C语言依赖开发人员管理内存提高灵活性但也带来了内存泄漏(OutOfMemeory内存泄漏指是指由于疏忽或错误造成程序未能释放已经不再使用的内存。主要指指针跑飞)和内存溢出(StackOverflow内存溢出是指系统在为某段执行指令程序分配内存时发现剩余内存不足并抛出错误。主要指栈溢出)的隐患。严格依赖于程序员的对内存的认知水平。 为1.屏蔽开发人员对内存的直接操作2.保证内存的安全使用JVM承担了这部分职责。 尽管JVM自动内存管理机制已经很完善但是仍可能存在内存泄漏或内存溢出的问题。为增加在这类问题出现后的处理能力有必要熟悉JVM的内存管理机制。 内存管理概述 Java 虚拟机运行时数据区划分为堆、方法区、程序计数器、虚拟机栈、本地方法栈。其概述图如下 对于C语言内存区可以发现JVM在C语言的基础上进行了调整以配合Java语言面向对象、支持原生调用、支持多线程等特性。 Java 堆Java Heap Java堆是供绝大多数类实例和数组对象分配内存的区域。神域的小部分类实例分配在方法区–常量引用的对象 Java堆是各个线程共享的运行时内存区域。 Java堆是垃圾收集器GC管理的主要对象因此也被称为“GC堆”Garbage Collected Heap注意这里未翻译成“垃圾堆”。 JVM规定Java堆可以处于物理上不连续的内存空间中只要逻辑上是连续即可。物理可不连续逻辑必须连续 方法区Method Area 方法区用于存储已被虚拟机加载的类的结构信息、运行时常量池、编译后的代码等。 方法区是所有线程共享的内存区域与堆类似。 方法区存储类数据Java堆存储对象数据。类加载时主要和方法区交互对象分配时主要和Java堆交互。 运行时常量池 运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述等信息外还有一项信息是常量池用于存放编译期生成的各种字面量和符号引用这部分内容将在类加载后存放到运行时常量池中。 同时运行时的常量也会放到这里。如String类型的intern()方法。 运行时常量池与C语言的“数据区”类似。 程序计数器Program Counter Register 程序计数器可看作当前线程所执行的字节码的行号指示器。所谓字节码指示器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 每个线程都有一个独立的程序计数器各线程之间的计数器互不影响独立存储这类内存区域为“线程私有”内存。 Java 栈Java Stack Java 栈描述Java方法执行的内存模型每个方法执行的时候都会创建一个栈帧Stack Frame。栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等方法执行相关信息。方法的调用和完成过程对应一个栈帧在JVM栈中从入栈到出栈的过程。 Java 栈与程序计数器一样也是线程私有它的生命周期与线程相同。 本地方法栈Native Method Stack JVM根据方法的实现语言将方法分为两类Java方法和Native方法。其中Native方法特指C/C方法。Java方法在Java 栈中存储、调用。Native方法则在本地方法栈中存储、调用。 本地方法栈与Java 栈一样也是线程私有它的生命周期与线程相同。 直接内存 直接内存不是虚拟机运行时数据区的一部分。在JDK 1.4 新加入NIONew Input/Output类时引入了一种基于通道Channel和缓冲区Buffer的I/O方式。其使用Native函数库直接分配内存然后通过一个存储在Java对立面的DirectByteBuffer对象作为这块内存的引用进行操作。 直接内存不受Java 堆的大小影响但是会受到本机总内存大大小及处理器寻址空间的限制。 HotSpot对象内存处理 作为最流行的虚拟机研究HotSpot对象在内存的处理过程具有实用价值。虚拟机中对象的处理主要三个部分对象创建、对象布局、对象访问。 1. 对象创建 Java中对象创建在Java语言层面仅仅是一个new关键字。而当虚拟机遇到一条new指令时会进行一序列对象创建的操作。HotSpot虚拟机中对象创建主要分为五步1加载类2分配内存3初始化零值4设置对象头5执行Init方法。 1加载类 当虚拟机遇到一条new指令时首先会去检查该指令的参数能否在常量池中定位到这个类的符号引用并且检查这个符号引用代表的类是否已被加载、解析、初始化过如果没有则必须先执行相应的类加载过程。 2分配内存 类加载检查完成后虚拟机将为新对象分配内存空间。该过程就是在堆中划分一小部分确定大小的空间用于存储对象信息。其中分配方式有以下两种指针碰撞和空闲列表。指针碰撞是指在内存绝对规整的前提下所有用过的内存放在一边空闲的内存放在另一边中间放着一个指针作为分界点的指示器。而分配内存就是将指针向空闲空间挪动一段与对象大小相等的距离。空闲列表则假定内存不规整无法简单地进行指针碰撞此时必须维护一个列表记录可用内存块。内存是否规整与垃圾收集器是否带有压缩整理功能决定。 另一个需要考虑的问题就是并发场景下内存分配。在并发场景下可能存在给对象A分配内存指针未及时修改对象B又同时使用原来的指针来分配内存的情况。解决这个问题的方案有两种一种是“CAS失败重试”保证更新操作的原子性另一种是优先使用TLABThread Local Allocation Buffer线程本地分配缓存TLAB是指每个线程在Java堆中预先分配的一小块内存。 3初始化零值 内存分配完毕后虚拟机将该对象分配的内存空间全部设置初始值零不包含对象头部分该操作可以保证对象的实例字段在代码中即使不赋予初始值也可以直接使用。程序能访问到这些字段的数据类型所对应的零值(不包括对象头)。 4设置对象头 初始化零值完成后虚拟机将对象的一些必要信息存放在对象头中这些信息包括例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。另外根据虚拟机当前运行状态的不同如是否启用偏向锁等对象头会有不同的设置方式。 5执行Init方法 虚拟机完成一个对象的创建后对于java程序而言对于该对象的一些定制的内容还未进行方法中包含了程序员的定制需求和意愿执行完init方法后对象完成了初始化此时才是一个可用对象。 2. 对象布局 虚拟机中对象在内存中的存储包括三个部分对象头、实例数据和对齐填充。 对象头 对象头包括两部分信息第一部分用于存储对象自身运行时数据哈希码、GC分代年龄、锁状态标志等等另一部分是类型指针即对象指向它的类元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。 实例数据 实例数据是对象真正存储的有效信息也是在程序中所定义的各种类型的字段内容。 对齐填充 该部分不是必然存在的仅起占位作用可类比C语言的结构体的对齐特性。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数1倍或2倍因此当对象实例数据部分没有对齐时就需要通过对齐填充来补全。 3. 对象访问 Java程序通过java栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定目前主流的访问方式有两种(1)使用句柄(2)使用直接指针。 两种访问方式各有优势使用句柄访问的最大好处是reference中存储的是稳定的句柄地址在对象被移动时垃圾回收时移动对象是非常普遍的行为只需改变句柄中的实例数据的指针而reference本身不需要调整。 使用直接指针访问方式的最大好处就是速度更快它节省了一次指针定位的时间开销由于对象的访问在Java中非常频繁因此这类开销积少成多后也是一项非常可观的执行成本。 HotSpot使用第二种方式进行对象访问但从整个软件开发的范围开看使用句柄访问的方式也很常见。 垃圾回收 如何判断对象存活 在研究对象的回收之前我们需要先看一下如何进行判断对象是否还有存活价值即要先判断对象是否还有被引用。 堆中几乎存放着Java世界中所有的对象实例垃圾收集器在对堆回收之前第一件事情就是要确定这些对象哪些还“存活”着哪些对象已经“死去”(即不可能再被任何途径使用的对象)。在主流的商用程序语言中(Java和C#等)都是使用可达性分析算法(Reachability Analysis)来判断对象是否存活的但是又有很多人认为是用引用计数算法(Reference Counting)来判断。接下来将 分别介绍这两种算法。 引用计数Reference Counting算法 引用计数算法实现思想如下给对象中添加一个引用计数器每当有一个地方引用该对象时其计数器值就加1当引用失效时计数器值就减1任何时刻计数器为0的对象就是不可能再被使用的。 引用计数算法实现简单判定高效在大部分情况下她都是一个不错的算法也有一些比较著名的应用案例如微软公司的COMComponent Object Model技术、Python语言和在游戏脚本领域被广泛应用的Squirrel都使用引用计数算法进行内存管理。但是引用计数算法无法解决对象之间循环引用的问题也即ABBA问题。 之所以在COM技术、Python等使用该技术是因为其已找到对应的解决策略或定位循环引用的方法。能想到的一种简单的方法是通过分层的概念来避免循环引用。强制约束同层之间不能相互引用。也可以通过判断算法判断是否存在ABBA这种场景。还有一种方法就是规定一些根结点这个根结点只能被外部引用不存在引用他人的场景这里将其成为引用原子性这种算法思想也是“可达性分析算法”的基础。 可达性分析Reachability Analysis算法 在主流的商用程序语言中都是通过可达性分析来判断对象是否存活的。这个算法的思想是通过一系列的称为“GC Roots”的对象作为起始点从这些节点开始往下搜索搜索所走过的路径称为“引用链”当一个对象到GC Roots没有任何引用链相连接时用图论的话来说就是从GC Roots到这个对象不可达就证明此对象是不可用的。示例如下 在Java语言中可以作为GC Roots对象的有 1虚拟机栈栈帧的本地变量表中引用的对象 2本地方法栈中JNINative方法引用的对象 3方法区中类静态属性static修饰引用的对象 4方法区中常量final修饰引用的对象。 引用级别划分 引用计数算法和可达性分析算法通过管理引用来判断该对象是否存活。为了更好的进行内存管理丰富对象的引用状态更好的刻画现实世界在JDK 1.2后Java对引用的概念进行了扩充。根据引用对应垃圾回收的力度引用可分为四种。从强到弱依次是强引用Strong Reference、软引用Soft Reference、弱引用Weak Reference、虚引用Phantom Reference。 1强引用 如果一个对象具有强引用那垃圾回收器绝不会回收它。使用方式如下 Object o new Object(); // 强引用如果不使用尽量通过如下方式来弱化引用 o null; // 帮助垃圾收集器回收此对象显式地设置o为null或超出对象的生命周期范围则gc认为该对象不存在引用就可回收对象。 在方法内部的强引用会在方法运行完成后退出虚拟机栈或本地方法栈引用消失。此后这个Object可被回收。如果这个对象是全局变量就需在不用这个对象时赋值为null因为强引用不会被垃圾回收。 2软引用 如果一个对象只具有软引用当内存空间不足时该引用对应的对象的内存将被回收。软引用可用来实现内存敏感的高速缓存。 String strnew String(abc); SoftReferenceString softRefnew SoftReferenceString(str); // 软引用当内存不足时等价于 String strnew String(abc); if(JVM.内存不足()) {str null; // 转换为软引用System.gc(); // 垃圾回收器进行回收 }软引用在实际中有重要的应用例如浏览器的后退按钮。按后退时这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢这就要看具体的实现策略了1如果一个网页在浏览结束时就进行内容的回收则按后退查看前面浏览过的页面时需要重新构建2如果将浏览过的网页存储到内存中会造成内存的大量浪费甚至会造成内存溢出。这时就可以使用软引用。实例如下 Browser prev new Browser(); // 获取页面进行浏览 SoftReference sr new SoftReference(prev); // 浏览完毕后置为软引用 if(sr.get()!null){ rev (Browser) sr.get(); // 还没有被回收器回收直接获取 }else{prev new Browser(); // 由于内存吃紧所以对软引用的对象回收了sr new SoftReference(prev); // 重新构建 }3弱引用 如果一个对象只具有弱引用不管内存空间足够与否都会在执行GC时回收对应内存。使用方式如下 String strnew String(abc); WeakReferenceString abcWeakRef new WeakReferenceString(str);执行GC时等价于 String strnew String(abc); str null; System.gc();如果这个对象是偶尔的使用并且希望在使用时随时就能获取到但又不想影响此对象的垃圾收集那可用 Weak Reference 来引用此对象。 弱引用可以和一个引用队列ReferenceQueue联合使用如果弱引用所引用的对象被垃圾回收Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。当你想引用一个对象但是这个对象有自己的生命周期你不想介入这个对象的生命周期这时候你就是用弱引用。 这个引用不会在对象的垃圾回收判断中产生任何附加的影响。 4虚引用 如果一个对象只具有虚引用那么在任何时候都有可能被GC。与其他几种引用都不同虚引用并不会决定对象的生命周期。虚引用主要用来跟踪对象被垃圾回收器回收的活动从而在对象被垃圾回收器回收时收到一个系统通知。 虚引用与软引用和弱引用的一个区别在于虚引用必须和引用队列ReferenceQueue联合使用。当垃圾回收器准备回收一个对象时如果发现它还有虚引用就会在回收对象的内存之前把这个虚引用加入到与之关联的引用队列中。 垃圾收集算法 不同垃圾回收器可能采用不同的垃圾回收算法。这里重点介绍几种常用的垃圾回收算法。 标记-清除Mark-Sweep算法 标记-清除算法将垃圾回收分为两个阶段标记阶段和清除阶段。该算法的思想是首先标记出所有需要回收的对象然后统一回收所有被标记的对象。 标记-清除算法存在两点不足1.标记和清除过程效率不高2.会产生空间碎片问题。 1.标记和清除过程效率低下问题 使用“对象存活检测算法”对需要回收的对象进行标记的过程以及从Java堆中遍历需要被清除的对象并进行清除。因需要进行遍历所以效率不高。 2.空间碎片问题 清除可回收对象仅仅是将该对象占用的内存回收所以会产生不连续空间碎片。当需要分配较大对象时无法找到足够的内存而不得不提前触发另一次垃圾回收。 标记-清除算法使用的方法是最简单的。但标记阶段完成后未被访问到的对象需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的性能。 标记-清除算法执行过程实例如下 复制算法 复制算法是为了解决标记-清除算法的效率问题。其思想如下将可用内存的容量分为大小相等的两块每次只使用其中的一块当其中一块内存用完或不足以分配给下一个对象时就将该内存块中存活的对象复制到另一个内存块上面然后再把该内存块的空间清除。 优点 内存分配时顺序分配内存无需考虑内存碎片问题。实现简单运行高效。 缺点 该算法的代价是将内存缩小为原来的一半内存利用率过低。 复制算法执行实例如下 现代商业虚拟机都采用该算法来回收新生代。由于新生代中绝大部分对象存活时间较短所以无需按照1:1的比例来划分内存空间。而是将内存分为较大的Eden空间和两块较小的Survivor空间每次使用Eden和其中一块Survivor。HotSpot虚拟机中默认Eden和Survivor的大小比例是81。 Eden 区 IBM 公司的专业研究表明98%的对象是朝生夕死。所以大多数情况下对象会在Eden分配当Eden没有足够空间时虚拟机会发起一次Minor GC。在Minor GC中Eden 会被清空对于无需回收的对象将会进入到Survivor的From区。 Survivor 区 Survivor 区相当于Eden 和 Old 区的缓冲暂存存活时间不是很多的对象。 1.为什么需要 Survivor 区 Eden 区回收后存活对象如果直接进入Old区会导致Old 区很快沾满且存活对象在第二次或三次回收时就可被清除。 Survior 区进行预筛选只有经历16次Minor GC的对象才能被送到老年代。 一个例外如果Survivor 区不足以存放Eden区和另外一个Survivor区的存活对象那么这些对象将直接进入Old区。 2.为什么需要两个Survivor区 如果只有一个Survivor区在执行Minor GC时既要考虑将Eden 区存活对象放置到该Survivor区还需要考虑Survivor区中剩余存活对象。因为这种场景下只能使用标记清除算法会带来内存碎片问题。 如果有两个Survivor区则可将Eden 区存活对象和其中一个Survivor区的剩余存活对象上一次Minor GC存活对象的再次筛选或存活的对象复制到另一个Survivor区。 3.为什么不是多个Survivor区 Survivor区数量越多每个Survivor区的Size就越小越容易导致Survivor区满。两个Survivor区是权衡后的最佳方案。 (4) 无需回收的对象的Size大于Survivor怎么办 如果无需回收对象的Size大于SurvivorSurvivor无法完全存储无需回收的对象部分无法安置的对象会直接进入老年代。内存担保机制 标记-整理算法 复制收集算法在对象存活率较高时就要进行较多的复制操作效率将会变低。更关键的是如果不想浪费50%的内存空间就需要额外的空间进行分配担保以应对内存中所有对象都100%存活的极端情况所以在老年代一般不能直接选用这种算法。 标记-整理算法与标记-清除算法类似只是在标记后不是对未标记的内存区域进行清理而是让所有的存活对象都向一端移动然后清理掉边界外的内存。 标记-整理算法实例如下 分代收集算法 分代收集算法就是根据对象存活周期的不同将内存划分几块。一般是将Java堆分为老年代Tenured Generation和新生代Young Generation在堆区之外还有一个代就是永久代Permanet Generation。这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中每次垃圾收集时都发现有大批对象死去只有少量存活经常选用复制算法。 在老年代中因为对象存活率高、没有额外空间对它进行分配担保就必须采用“标记-清除”或“标记-整理”算法来进行回收。 分代收集算法实例如下 Java堆主要分为两个区域-新生代和老年代其中新生代占据1/3的内存空间老年代占据2/3的内存空间。 HotSpot内存回收简介 商用虚拟机在实现GC时必须对算法的执行效率进行考量以保证虚拟机高效运行。在发起内存回收时HotSpot采用以下优化策略枚举根节点、定义安全点、定义安全区域。 可达性分析算法虽然能定位可回收对象但是存在以下问题 1.明确GC Roots耗时 可达性分析的基础是明确GC Roots节点。可作为GC Roots的节点主要在全局性的引用例如常量或类静态属性与执行上下文例如栈帧中的本地变量表中如果要逐个检查这里面的引用那么必然会消耗很多时间。 2.GC停顿不可避免 可达性分析对执行时间的敏感还体现在GC停顿上。因为这项分析工作必须在一个能确保一致性的快照中进行。这里的“一致性”是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上不可以出现分析过程中对象引用关系还在不断变化的情况该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有 Java执行线程Sun将这件事情称为“Stop The World”的其中一个重要原因即使是在号称几乎不会发生停顿的CMS收集器中枚举根节点时也是必须要停顿的。 枚举根节点 由于目前的主流Java虚拟机使用的都是准确式GC参考Exact VM对Classic VM的改进所以当执行系统停顿下来后并不需要一个不漏地检查完所有执行上下文和全局的引用位置虚拟机应当是有办法直接得知哪些地方存放着对象引用。 在HotSpot中是使用一组称为OopMap的数据结构来达到这个目的的在类加载完成的时候HotSpot就把对象内什么偏移量上是什么类型的数据计算出来在JIT编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样GC在扫描时就可以直接得知这些信息了。 这样算法的时间复杂度就从On优化到O1且不会占用过多的额外内存。 定义安全点Safepoint OopMap内容变化的指令非常多如果为每一条指令都生成对应的OopMap那将会需要大量的额外空间这样GC的空间成本将会变得很高。 实际上HotSpot并没有为每条指令都生成OopMap而是在“特定的位置”记录这些信息这些位置称为安全点Safepoint即程序执行时并非在所有地方都能停顿下来开始GC只有在到达安全点时才能暂停。 Safepoint的选定既不能太少以致于让GC等待时间太长也不能过于频繁以致于过分增大运行时的负荷。安全点的选定是以“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂程序不太可能因为指令流长度太长这个原因而过长时间运行“长时间执行”的最明显特征就是指令序列复用例如方法调用、循环跳转、异常跳转等所以具有这些功能的指令才会产生Safepoint。 对于Sefepoint还需要考虑的问题是如何在GC发生时让所有线程这里不包括执行JNI调用的线程都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择抢先式中断Preemptive Suspension和主动式中断Voluntary Suspension。 抢先式中断不需要线程的执行代码主动去配合在GC发生时首先把所有线程全部中断如果发现有线程中断的地方不在安全点上就恢复线程让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。 主动式中断是当GC需要中断线程的时候不直接对线程操作仅仅简单地设置一个标志各个线程执行时主动去轮询这个标志发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的另外再加上创建对象需要分配内存的地方。 定义安全区域Safe Region Safepoint机制保证在程序执行时在不太长的时间内就会遇到可进入GC的Safepoint。但是程序“不执行”没有分配CPU时间也即线程处于Sleep状态或者Blocked状态这时线程无法响应JVM的中断请求“走”到安全的地方去中断挂起JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况就需要安全区域Safe Region来解决。 安全区域是指在一段代码片段之中引用关系不会发生变化在这个区域中的任意地方开始GC都是安全的。 在线程执行到Safe Region代码时首先标识自己已经进入了Safe Region。当在这段时间里JVM要发起GC时就不用管标识为Safe Region状态的线程。在线程要离开Safe Region时要检查系统是否已经完成根节点枚举或者是整个GC过程如果完成了那线程就继续执行否则它就必须等待直到收到可以安全离开Safe Region的信号为止。 常用垃圾收集器简介 如果说收集算法是内存回收的方法论那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范对垃圾收集器应该如何实现并没有任何规定。基于JDK1.7 Update 14 之后的HotSpot虚拟机所包含的收集器如下图所示 可见HotSpot采用了七种种垃圾收集器。如果两个收集器之间存在连线就说明它们可以搭配使用。Hotspot之所以实现如此多的收集器是因为目前并无完美的收集器出现只能选择对具体应用最适合的收集器。 Serial收集器 Serial串行收集器是最基本、发展历史最悠久基于复制算法的新生代收集器是JDK 1.3.1之前新生代收集器的唯一选择。它是一个单线程收集器只会使用一个CPU或一条收集线程去完成垃圾收集工作更重要的是它在进行垃圾收集时必须暂停其他所有的工作线程直至Serial收集器收集结束为止“Stop The World”。这项工作是由虚拟机在后台自动发起和自动完成的在用户不可见的情况下把用户正常工作的线程全部停掉这对很多应用来说是难以接受的。Serial收集器的运行示意图如下 尽管“Stop The World”会导致服务不可用且HotSpot开发团队为消除或减少停顿而不断努力从Parallel收集器到Concurrent Mark Sweep收集器再到Garbage first收集器在桌面级别应用场景待收集内存不会太大停顿时间完全可控制在几十毫秒最多一百多毫秒。而且作为单线程收集器Serial收集器可以获得最高的单线程收集效率。 ParNew收集器 ParNew同样用于新生代是Serial的多线程版本并且在参数、算法同样是复制算法上也完全和Serial相同。 Par是Parallel的缩写但它的并行仅仅指的是收集多线程并行并不是收集和原程序可以并行进行。ParNew也是需要暂停程序一切的工作然后多线程执行垃圾回收。ParNew收集器的工作过程如下图老年代采用Serial Old收集器 ParNew收集器相比Serial收集器仅实现基于多线程的GC但它却是Server模式下的首选新生代收集器。其中一个与性能无关的重要原因是除了Serial收集器外目前只有它能和CMS收集器配合工作。 ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果甚至由于存在线程交互的开销该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下随着CPU的数量增加它对于GC时系统资源的有效利用是很有好处的。 Parallel Scavenge 收集器 Parallel Scavenge收集器也是一个并行的多线程新生代收集器它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量Throughput。停顿时间越短就越适合需要与用户交互的程序良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间尽快完成程序的运算任务主要适合在后台运算而不需要太多交互的任务。 支持参数控制以及自适应调节策略是Parallel Scavenge收集器与ParNew收集器的一个重要区别。 Serial Old收集器 Serial Old 是 Serial收集器的老年代版本它同样是一个单线程收集器使用“标记-整理”Mark-Compact算法。 此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下它还有两大用途 1在JDK1.5 以及之前版本Parallel Old诞生以前中与Parallel Scavenge收集器搭配使用。 2作为CMS收集器的后备预案在并发收集发生Concurrent Mode Failure时使用。 它的工作流程与Serial收集器相同。 Parallel Old收集器 Parallel Old收集器是Parallel Scavenge收集器的老年代版本使用多线程和“标记-整理”算法。该收集器是在JDK 1.6中才开始提供的在此之前如果新生代选择了Parallel Scavenge收集器老年代除了Serial Old以外别无选择。在注重吞吐量以及CPU资源敏感的场合都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图 CMSConcurrent Mark Sweep收集器 CMSConcurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用这些应用都非常重视服务的响应速度。从名字上“Mark Sweep”就可以看出它是基于“标记-清除”算法实现的。CMS收集器工作的整个流程分为以下4个步骤 1初始标记Initial mark仅标记GC Roots能直接关联到的对象速度很快准确式内存需要“Stop The World”。 2并发标记Concurrent mark进行GC Roots Tracing的过程在整个过程中耗时最长并发执行。 3重新标记Remark修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间一般会比初始标记阶段稍长一些但远比并发标记的时间短。此阶段也需要“Stop The World”。 4并发清除Concurrent sweep对已标记的垃圾进行GC并发执行。 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作所以从总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。CMS收集器的运作步骤中并发和需要停顿的时间 CMS是一款优秀的收集器起主要优点体现在并发收集、低停顿因此CMS收集器也被称为并发低停顿收集器Concurrent Low Pause Collector。 但是CMS收集器存在以下缺点 1对CPU资源非常敏感。因面向并发设计程序所以对CPU资源比较敏感。在多核时代这个缺点已转换成优点。但在单核处理器场景下则要慎重考虑。 2无法处理浮动垃圾Floating Garbage。 “浮动垃圾”是指在CMS并发清理阶段用户线程运行产生的新垃圾。这一部分垃圾出现在标记之后CMS无法在当次收集中处理掉它们只好留待下一次GC时再清理掉。“浮动垃圾”会导致出现“Concurrent Mode Failure”进而引发另一次Full GC。同时由于垃圾收集阶段用户线程还需运行所以必须预留足够的内存空间给用户线程使用。因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集需要预留一部分空间提供并发收集时的程序运作使用。 3标记-清除算法导致的空间碎片。CMS是一款基于“标记-清除”算法实现的收集器这意味着收集结束时会有大量空间碎片产生。空间碎片过多时将会给大对象分配带来不便。可以通过设置参数决定执行合并整理的时机。 G1收集器 G1Garbage-First收集器是当今收集器技术发展最前沿的成果之一它是一款面向服务端应用的可预测停顿时间的垃圾收集器。G1具备如下特点 1并行与并发。G1 能充分利用多CPU、多核环境下的硬件优势使用多个CPU来缩短“Stop The World”停顿时间。 2分代收集。分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。 3空间整合。G1从整体来看是基于“标记-整理”算法的收集器从局部两个Region之间上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片收集后能提供规整的可用内存。此特性有利于程序长时间运行分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。 4可预测的停顿。这是G1相对CMS的一大优势降低停顿时间是G1和CMS共同的关注点但G1除了降低停顿外还能建立可预测的停顿时间模型能让使用者明确指定在一个长度为M毫秒的时间片段内消耗在GC上的时间不得超过N毫秒这几乎已经是实时JavaRTSJ垃圾收集器的特征。 横跨整个堆内存。G1将整个Java堆划分为多个大小相等的独立区域Region虽然还保留新生代和老年代的概念但新生代和老年代不再是物理隔离而都是一部分Region不需要连续的集合。 建立可预测的时间模型。G1收集器之所以能建立可预测的停顿时间模型是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小回收所获得的空间大小以及回收所需时间的经验值在后台维护一个优先列表每次根据允许的收集时间优先回收价值最大的Region这也就是Garbage-First名称的来由。这种使用Region划分内存空间以及有优先级的区域回收方式保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。 避免全堆扫描——Remembered Set。G1把Java堆分为多个Region就是“化整为零”。但是Region不可能是孤立的一个对象分配在某个Region中可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候需要扫描整个Java堆才能保证准确性这显然是对GC效率的极大伤害。为了避免全堆扫描的发生虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时会产生一个Write Barrier暂时中断写操作检查Reference引用的对象是否处于不同的Region之中在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。 G1收集器的运作大致可划分为以下几个步骤 1初始标记Initial Mark。仅标记 GC Roots 能直接关联到的对象并且修改TAMSNest Top Mark Start的值让下一阶段用户程序并发运行时能在正确的Region中创建对象此阶段需要停顿线程Stop the World但耗时很短。 2并发标记Concurrent Mark。从GC Root 开始对堆中对象进行可达性分析找到存活对象此阶段耗时较长但可与用户程序并发执行。 3最终标记Final Mark。为修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中这阶段需要停顿线程Stop the World但可并行执行。 4筛选回收Live Data Count and Evacuation。 对各个Region中的回收价值和成本进行排序根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行但是因为只回收一部分Region时间是用户可控制的而且停顿用户线程将大幅度提高收集效率。 G1 收集器的运作步骤如下 Java动态编译 动态编译运行时编译是指程序在运行时加载类并进行编译。 动态编译使用场景 动态编译的应用场景很多典型的场景就是一个需要外部输入Java代码的场景如Leetcode等在线算法测试平台。 动态编译实现 早期实现动态编译主要使用反射实现。JDK 1.6后也可以基于动态编译API实现。 动态编译的实现 (1) 通过Runtime调用javac命令启动新的进程去编译Java代码。示例代码如下 Runtime run Runtime.getRuntime(); Process process run.exec(javac -cp d:/myjava/Helloworld.java)(2) 通过JavaCompiler动态编译。 使用JDK 1.6新增的JavaCompiler API。示例代码如下 JavaCompiler compiler ToolProvider.getSystemJavaCompiler(); int result compiler.run(null, null, null, f:/HelloWorld.java); System.out.println(result0?编译成功:编译失败);完成动态编译后接下来就可以运行该类中的接口。方式一是使用另外一个进程执行方式二是使用反射实现。方式一示例代码如下 JavaCompiler compiler ToolProvider.getSystemJavaCompiler(); int result compiler.run(null, null, null, f:/HelloWorld.java); System.out.println(result0?编译成功:编译失败); Runtime run Runtime.getRuntime(); Process process run.exec(java -cp f: HelloWorld);BufferedReader w new BufferedReader(new InputStreamReader(process.getInputStream())); System.out.println(w.readLine());方式二示例代码如下 public class DynamicCompile {public static void main(String[] args) throws IOException {JavaCompiler compiler ToolProvider.getSystemJavaCompiler();int result compiler.run(null, null, null, f:/HelloWorld.java);System.out.println(result0?编译成功:编译失败);try {URL[] urls new URL[]{new URL(file:/f:/)};URLClassLoader loader new URLClassLoader(urls);Class? c loader.loadClass(HelloWorld);Method m c.getMethod(main, String[].class);m.invoke(null, (Object)new String[]{});//静态方法不用谢调用的对象//加Object强制转换的原因//由于可变参数是JDK5.0之后才有 m.invoke(null, new String[]{23,34});//编译器会把它编译成m.invoke(null,23,34);的格式,会发生参数不匹配的问题//带数组的参数都这样做} catch (Exception e) {e.printStackTrace();}} }outofmemory和stackoverflow 内存泄漏OutOfMemeory是指由于疏忽或错误造成程序未能释放已经不再使用的内存。主要指指针跑飞。如数组越界。 内存溢出StackOverflow是指系统在为某段执行指令程序分配内存时发现剩余内存不足并抛出错误。主要指栈溢出。 参考 《深入理解Java虚拟机 JVM高级特性与最佳实践 第2版》 周志明 著 https://segmentfault.com/a/1190000016842546 Java动态性(1) - 动态编译(DynamicCompile) https://javabeat.net/the-java-6-0-compiler-api/ The Java 6.0 Compiler API https://www.cnblogs.com/jxrichar/p/4883465.html 动态生成java、动态编译、动态加载 https://www.cnblogs.com/hbuwdx/p/9489177.html Java动态编译技术原理 https://blog.csdn.net/ShuSheng0007/article/details/81269295 秒懂Java动态编程Javassist研究 https://blog.csdn.net/wangjian530/article/details/83449067 Java动态编译JavaCompiler https://www.throwx.cn/2020/06/06/java-dynamic-compile/ 深入理解Java的动态编译 https://blog.csdn.net/m0_37556444/article/details/81912283 如何破坏双亲委派模型 https://www.jianshu.com/p/bfa495467014 破坏双亲委派机制的那些事 https://blog.51cto.com/westsky/1579033 OSGI(面向Java的动态模型系统)和它的实现Equinox https://segmentfault.com/a/1190000008722128 JVM 虚拟机字节码指令表 https://www.cnblogs.com/wade-luffy/p/6058067.html 运行时栈帧结构 https://cloud.tencent.com/developer/article/1894284 什么是编译执行和解释执行 https://segmentfault.com/a/1190000016640854 JVM系列2HotSpot虚拟机对象 https://blog.csdn.net/u011080472/article/details/51324103 【深入理解JVM】垃圾收集算法 https://www.jianshu.com/p/50d5c88b272d 深入理解JVM5 : Java垃圾收集器 https://www.cnblogs.com/1024Community/p/honery.html 扒一扒JVM的垃圾回收机制 https://mp.weixin.qq.com/s/feJKRqYJTVEIxl6jvjevAg 咱们从头到尾说一次 Java 的垃圾回收
http://www.hkea.cn/news/14522716/

相关文章:

  • 用html5设计个人网站公司的网站打不开
  • 彩票网站开发dadi163wordpress批量修改图片标题
  • 一个网站的建设需要哪些流程图我的企业网站怎么seo
  • 常州免费网站建站模板wordpress添加顶部导航条
  • 随州北京网站建设网络营销的目的和意义
  • 制作企业网站用什么软件合肥知名网站制作公司
  • 淘宝导购网站建设小程序营销策略
  • wordpress点评系统淘宝seo是指什么
  • 重庆 网站设计上海市建设工程咨询有限公司
  • 杭州seo博客深圳网站seo优化
  • 体育设施建设发布有没有网站php网站开发教程 pdf
  • 广州做手机网站信息国家域名备案查询
  • iis6.1添加网站wordpress 无法更换会员注册页面
  • 做网站设计要注意什么问题高端的的网站建设公司
  • 关于asp sql网站开发的书籍prower wordpress
  • 网站怎么做引流珠海手机网站
  • 建设网站通过什么赚钱为什么要先创建站点后建立文件?能否改变两者的顺序?
  • 做网站搞什么流量seo顾问收费
  • 自建站 外贸wordpress 提速插件
  • 外贸网站开发公司网站建设丶金手指花总12
  • 网站后缀有哪些建设网站不显示添加白名单
  • 高端网站建设方案范文湖北建设厅官方网站
  • 网站建设目的要求南阳公司注册
  • 怎么给一个网站做推广怎么创立一个自己的品牌
  • 小企业建网站关闭WordPress自动文章摘要
  • 河南亿元建设有限公司公司网站wordpress 获得分类
  • 一家专门做鞋子的网站寮步网站建设 优帮云
  • 网站建设的公司推荐居众装饰
  • 鹿泉城乡建设局网站易思网站系统
  • 越秀定制型网站建设asp网站做文件共享上传