深入理解 Jvm 读书笔记(二)

Jvm 类加载及执行引擎相关

知识包括:

  • jvm类加载机制
    • 类加载时机及过程
    • 类加载器 双亲委派模型及如何破坏;
  • jvm字节码执行引擎
    • 运行时栈帧结构
    • 方法调用等分析
    • 基于栈,基于寄存器指令集
  • 类加载及执行子系统的案例与实战介绍
  • 程序编译与代码优化介绍

Jvm类加载机制

代码编译的结果是从本地机器码转变为字节码,jvm把描述类的数据从Class文件(二进制字节流)加载到内存,并对数据进行校验,解析和初始化,最终可以被jvm直接使用的java类型;

类加载的时机

类的生命周期

加载(Loading) -> [验证(Verification) -> 准备(Preparation) -> 解析(Resolution)] -> 初始化 (Initialization) -> 使用(Using) -> 卸载(Unloading)

  • 加载,验证,准备,初始化,卸载的顺序确定的;解析阶段不一定,为了支持java的运行时绑定(动态绑定,晚期绑定); 这些阶段通常都是相互交叉混合式的进行的;

  • 有且只有以下情况,没有类初始化需要进行类的初始化,简称对一个类的主动引用:

    • 遇到 new,getstatic,putstatic或invokestatic 字节码指令;分别对应 实例化对象,读取设置一个类的静态字段(被final修饰,已在编译器吧结果放入常量池的静态字段除外,因为使用的是ConstantValue初始化而不是方法),以及调用类的静态方法;
    • 使用java.lang.reflect 包的方法对类进行反射调用的时候
    • 当初始化一个类时,发现其父类还没有进行过初始化,需要先触发器父类的初始化;
    • 当jvm启动时,用户需要指定一个要执行的主类(包含main方法的类),jvm会先初始化这个主类;
    • 使用jdk 1.7动态语言支持时,一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄;
  • 被动引用不会导致类初始化;

    • 子类引用父类的静态字段,不会导致子类初始化;
    • 通过数组定义来引用类,不会触发此类的初始化;
      • 不过jvm会生成一个直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发(可否理解为可加载解析,不会初始化);
    • 常量(final , ConstantValue属性)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,不会触发定义常量类的初始化;
  • 接口的加载过程与类加载过程稍有不同

    • 接口在初始化时,并不要求父接口全部都完成了初始化,只有在真正使用到父接口(引用接口中定义的常量)采用初始化;

类加载的过程

  • 加载
    • 通过一个类的全限定名获取定义此类的二进制字节流;
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

加载阶段完成后,jvm外部的二进制字节流就按照jvm所需格式存储在方法区之中,然后在内存中(hotspot->方法区)实例化一个Class类对象,作为程序访问方法区中的这些类型数据的外部接口;


	- 实际上,jvm规范的这3条是非常灵活的
	
	非数组类的加载过程(加载阶段获取类的二进制字节流的动作)是开发人员可控性最强的;
	因为加载阶段既可以使用系统提供的引导类加载器完成,也可以使用用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方法(重写一个类加载器的loadClass方法)

	数组类而言,数组类本身不通过类加载器创建,由jvm直接创建的;但是数组类的元素类型(ElementType 数组去掉所有维度的类型)最终是要靠类加载器去创建;数组类的创建: 
		- 如果数组的组件类型(Component Type 数组去掉一个维度的类型)是引用类型,递归加载组件类型;数组C在加载改组件类型的类加载器的类名称空间上被标志;
		(一个类必须与类加载器一起确定唯一性)
		- 如果数组的组件类型不是引用类型(int[]数组),jvm会把数组C标记为与引导类加载器关联;
		- 数组类的可见性与它的组件类型的可见性一致,组件类型不是引用类型,数组类的可见性默认为public;
  • 验证 链接阶段的第一步,确保Class文件的字节流中包含的信息符合jvm的要求;

    • 文件格式验证
      • 验证字节流是否符合Class文件格式的规范,并且能被jvm处理;通过此阶段后,字节流才会进入内存的方法区中进行存储;
    • 元数据验证
      • 对字节码描述信息进行语义分析,符合java语言规范;
    • 字节码验证
      • 确定程序语义是合法的,符合逻辑的,jdk1.6后Code属性添加StackMapTable节省时间;
    • 符号引用验证
      • 验证jvm将符号引用转换为直接引用的时候,动作在解析阶段中发生;对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验;
      • 无法通过,会抛出IncompatibleClassChangeError异常的子类,如IllegalAccessError,NoSuchFieldError,NoSuchMethodError;
  • 准备

    • 正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配;
      • 内存分配的仅包括类变量,不包括实例变量;
      • 通常情况下赋初值指的是java的默认初始值,但是类字段的字段属性表中存在ConstantValue(final标记)属性,准备阶段的变量会被初始化ConstantValue属性所指定的值;

	public static int value = 123;

	准备阶段初始值为0,而不是123,赋值123是putstatic字节码指令被编译后,存放在类构造器<clinit>方法之中,所以赋值123是在初始化阶段才会执行;

	public static final int value =123;

	准备阶段初始值即为123,因为final修饰的字段,字段表中存在ConstantValue属性,在编译时javac将会为value生成ConstantValue属性,在准备阶段jvm根据ConstantValue的值将value赋值为123;
  • 解析

    • jvm将常量池内的符号引用替换为直接引用的过程;
    • 引用区分:
      • 符号引用 (Symbolic References): 以一组符号来描述所引用的目标,可以是任何形式的字面量,与jvm实现的内存布局无关;
      • 直接引用 (Direct References): 直接引用可以是直接指向目标的指针,相对偏移量或者一个能间接定位到目标的句柄; 如果有直接引用,则引用目标必定在内存中存在;
    • jvm规范中并未规定解析发生的具体时间,要求在执行anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield,putstatic16个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析;
    • invokedynamic 指令必须等到程序实际运行到这条指令的时候,解析动作才能进行,且不缓存;其余符号指令可以在完成加载阶段,还没开始执行就进行解析,也可对第一次解析结果进行缓存,避免解析重复进行;
    • 解析动作主要针对接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7种符号引用,对应于常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info,CONSTANT_MethodType_info,CONSTANT_MethodHandle_info,CONSTANT_invokeDynamic_info7中常量类型;
      • 类或接口的解析 (D:当前代码所处的类; N:从未解析过的符号引用;C:类或接口的直接引用;)
        • 如果不是一个数组类型,jvm将N的全限定名传递给D的类加载器加载这个类C;
        • 是一个数组类型, N的描述符类型添加[,按照1加载数组元素类型;
      • 字段解析 (C: 字段所属的类或接口) 先解析字段表;
        • C本身包含简单名称和字段描述符都匹配,返回字段直接引用,查找结束;
        • 如果C中实现了接口,按照继承关系从下往上递归搜索各个接口和它的父接口,接口中包含简单名称和字段描述符都匹配的字段,返回字段直接引用,查找结束
        • 否则,C不是java.lang.Object,按照继承关系从下往上递归搜索其父类,找到简单名称和描述符都匹配的字段,返回字段直接引用,查找结束;
        • NoSuchFieldError
      • 类方法解析 (C: 类) 先解析类方法表;
        • 类方法和接口方法符号引用的常量类型是分开定义的,发现定义不同抛出IncompatibleClassChangeError异常;
        • 在C中查找简单名称和描述符都匹配的方法,如果有返回直接引用,查找结束;
        • 在C的父类中递归查找…
        • 在C实现的接口列表及父接口中递归查找匹配的方法,存在说明C为抽象类,抛出AbstractMethodError异常(接口中存在static方法,不支持);
        • NoSuchMethodError
      • 接口方法解析 (C:类) 接口方法表;
        = 与类方法解析1相同;
        • 在接口C中查找是否有简单名称和描述符都匹配的方法,如果有返回直接引用,查找结束;
        • 在接口C的父接口递归查找,直到java.lang.Object类为止,有则返回,结束查找;
        • NoSuchMethodError
  • 初始化

类加载的最后一步,初始化阶段是执行类构造器<clinit>方法的过程;

  • <clinit> 方法特点
    • <clinit>方法 是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并生成的,收集顺序是由语句在源文件中出现的顺序决定;
      • 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问;
    • <clinit>()方法与类的构造函数(实例构造器<init>()方法) 不同,不需要显示的调用父类构造器;jvm会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕;
      • 因此 jvm中第一个被执行的<clinit>()方法的类肯定是java.lang.Object;
      • 由于父类的<clinit>()方法先执行,父类中定义的静态语句块要优先于子类的变量赋值操作;
    • <clinit>()方法对于类或接口不是必须的;
    • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,接口和类都会生成<clinit>方法; 其中,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法,只有到父接口中定义的变量使用时,父接口才会初始化;
    • jvm保证一个类的<clinit>()方法在多线程环境中被正确的加锁,同步,多个线程同时去初始化一个类,只有一个线程去执行这个类的<clinit>方法,其他线程阻塞等待,当线程退出<clinit>方法后,其他线程唤醒后也不会再次进入到<clinit>方法,因为同一个类加载器下,一个类型只会初始化一次;

类加载器

加载过程中 通过一个类的全限定名获取类的类的二进制字节流 可以让应用程序决定如何去获取所需的类 ,实现这个动作的代码模块为类加载器;

  • 类名称空间: 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在jvm中的唯一性; 每一个类加载器都拥有一个独立的类名称空间;

    • 即两个类相等,只有两个类由同一个类加载器加载的前提下才有意义;
    • 相等指的是代表类Class对象的equals,isAssignableFrom,isInstance,instanceOf等方法;
  • 双亲委派模式 Parents Delegation Model

    • 在jvm的角度说,只存在两种类加载器
      • 启动类(引导类)加载器(Bootstrap ClassLoader);
      • 其他类加载器(继承于java.lang.ClassLoader)
    • java开发人员的角度,分为3中
      • 启动类(引导类)加载器(Bootstrap ClassLoader)
        • C++实现,负责加载 <JAVA_HOME>\lib目录 或者被-Xbootclasspath参数所指定路径,并且是jvm识别的类库(仅按照文件名识别,如rt.jar)加载到jvm内存中;
        • 不可被java直接引用,用户在编写自定义类加载器,需要把加载请求委派给引导类加载器,直接使用null即可;
      • 扩展类加载器 (Extension ClassLoader)
        • sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定的路径中的所有类库;
        • 可被直接使用;
      • 应用程序类(系统类)加载器 (Application ClassLoader)
        • sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(ClassPath)上所指定的类库;
        • 可直接使用,如果没有自定义类加载器,一般就是程序默认类加载器;
      • BootstrapClassLoader <- ExtensionClassLoader <- ApplicationClassLoader <- CustomClassLoader 组合关系;
      • 如果一个类加载器收到类加载的请求,先委派给父类加载器完成,只有到父类加载器无法完成加载请求(搜索范围中没有找到所需的类),子加载器尝试自己加载;
  • 破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,双亲委派的破坏:

  • 第一次破坏: jdk 1.2之后添加 findClass方法;自定义类加载器逻辑写入findClass中,在loadClass方法的逻辑如果父类加载失败,则会调用自己的findClass方法完成加载,保证新写出来的类加载器是符合双亲委派规则的;
  • 第二次破坏: 模型自身的缺陷,双亲委派虽然能很好的解决各个加载器基础类的统一问题(越基础的类由越上层的加载器加载);但是问题是基础类调回用户的代码时, 比如JNDI由启动类加载器加载(rt.jar),Jndi调用某些需要由独立厂商实现并部署在应用程序的代码,启动类加载器加载的不能认识这些代码(还有JDBC等,因为是不同的类加载器加载,低层的可以认识顶层的,但顶层的不认识低层的);
    • 使用线程上下文类加载器兼容(Thread Context ClassLoader),通过Thread类setContextClassLoader方法设置,如果创建线程时还未设置,将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,默认类加载器就是应用程序类加载器;
    • JNDI服务使用线程上下文类加载器加载所需要的SPI代码(jndi接口提供者),也就是父类加载器请求子类加载器完成类加载的动作(我的理解是原本是启动类加载器加载jndi,spi由应用类加载器加载,导致不能访问;现在是设置上下文类加载器,jndi也是使用应用类加载器加载,spi由应用类加载器加载,同一个类加载器加载,可以访问;)
  • 第三次破坏: 用户对程序动态性的追求导致;如hotswap,热部署等;
    • OSGi java模块化标准,关键在于自定义的类加载器机制; 每一个程序模块(OSGi成为Bundle)都有一个自己的类加载器,需要更换一个Bundle时,把Bundle连同类加载器一起换掉实现代码的热部署;
    • OSGi为网状结构,不同于双亲的树状结构;

jvm字节码执行引擎

运行时栈帧结构

  • 栈帧 Stack Frame
    • 用于支持jvm进行方法调用和方法执行的数据结构,是jvm运行时数据区中的jvm栈的栈元素; 存储方法的局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息等信息;
    • 每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在jvm栈里面从入栈到出栈的过程;
    • 在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中,一个栈帧需要分配多少内存,仅仅取决于具体的jvm实现;
    • 在活动线程中,位于栈顶的栈帧才是有效的,称为当前栈帧(CurrentStackFrame),与这个栈帧相关联的方法称为当前方法(CurrentMethod),执行引擎运行的所有字节码指令都只针对当前栈帧进行操作;

在这里插入图片描述

  • 局部变量表 Local Variable Table

    • 一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,写入Code属性确定最大容量max_locals;
    • 以变量槽(Variable Slot) 为最小单位;每个slot都应该能存放boolean,char,byte,short,int,float,reference,returnAddress类型的数据;
      • reference 表示对一个对象实例的引用,直接或间接查找对象在java堆中数据存放的起始地址索引和在方法区中的存储的类型信息;
      • returnAddress 为jsr,jsr_w,ret服务,执行字节码指令的地址,实现异常跳转,现已经被异常表代替;
    • 对于64位的数据类型,jvm以高位对齐的方式分配两个连续的Slot空间; long和double都是64位数据类型(reference可能是32位也可能是64位),不能单独方位其中的某一个slot;
    • jvm 使用索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表的最大slot数量;
    • 在方法执行时,jvm使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static方法),局部变量表中第0位索引的slot默认是用于传递方法所属对象实例的引用,可以使用this来访问到这个隐含的参数;
    • slot是可以重用的,方法体中定义的变量,作用域并不一定覆盖整个方法体,如果当前字节码pc计数器的值已经超出了某个变量的作用域,那这个变量对应的slot就可以交给其他变量使用;
      • 不使用的对象手动赋值null 此处注意局部变量表slot如果在变量所处作用域之后,手动对对象设置null值并不是一个无意义的操作,因为可以去除slot对对象的引用,使对象提前被GC回收,而不是等到其他变量重用slot时在回收; 但是实际开发中并不需要赋值null;
  • 操作数栈 Operand Stack

    • 操作栈,后入先出(LIFO)栈,最大的写入Code属性确定最大容量max_stacks;
    • 操作数栈的每一个元素可以是任意的java数据类型,包括long和double;32位数据类型占栈容量为1,64位占有栈容量为2;
    • 当方法刚刚开始执行的时候,操作数栈是空的,执行过程中会有各种字节码指令往操作数栈中出栈/入栈; eg: 整数加法的字节码指令iadd运行时操作数栈最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将两个int值出栈并相加,然后将相加结果入栈;
    • jvm的解释执行引擎称为基于栈的执行引擎,栈就是操作数栈;

在这里插入图片描述

  • 动态链接 Dynamic Linking

    • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接;
    • Class文件的常量池存在大量符号引用,字节码中的方法调用指令以常量池中指向方法的符号引用作为参数;
      • 静态解析: 这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用
      • 动态连接: 另外一部分将在每一次运行期间转化为直接引用;
  • 方法返回地址

    • 当一个方法执行后,只有两种方法可以退出这个方法;

      • 执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion);
      • 在方法执行过程遇到了异常,并且这个异常在方法体内没有得到处理; 无论是jvm内部产生的异常还是代码中使用athorw字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,称为异常完成出口(Abrupt Method Invocation Completion);
    • 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作是: 恢复上层方法的局部变量表和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈中,调整pc计数器的值以指向方法调用指令后面的一条指令;

  • 附加信息 (实际开发中,一般将动态连接,方法返回地址,其他附加信息统称为栈帧信息;)

方法调用

方法调用不等同于方法执行,方法调用阶段的唯一任务是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程; java中Class文件存储的是符号引用,而不是具体地址,需要到类加载甚至到运行期间才能确定目标方法的直接引用;

  • 解析

    • 在类加载的解析阶段,会将一部分符号引用转化为直接引用,这种解析成立的前提是: 方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的; 也就是说,调用目标在程序代码写好,编译器进行编译时就必须确定下来的这类调用称为解析;
    • java提供5种方法调用字节码指令:
      • invokestatic: 调用静态方法;
      • invokespecial: 调用实例构造器<init>方法,私有方法,父类方法;
      • invokevirtual: 调用所有的虚方法(除final方法);
      • invokeinterface: 调用接口方法,会在运行时在确定一个实现此接口的对象;
      • invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法,然后在执行该方法,此条指令时由用户所设定的引导方法决定的,其他是固化在jvm内部;
      • 非虚方法有5类:
        • invokestatic,invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器,父类方法4类,在类加载的时候就会把符号引用解析为直接引用,这类方法称为非虚方法;
        • 非虚方法还有一类是被final修饰的方法,即使final方法是使用invokevirtual指令调用;其他的为虚方法;
      • 解析调用一定是个静态的过程,在编译期间就完全确定,类加载的解析阶段就会把符号引用转为直接引用,而分派可能是静态的也可能是动态的;
  • 分派 Dispatch

    • 多态性(重载&重写)
    • 静态分派 [编译阶段编译器的选择过程]
      • 静态类型&实际类型:
        • Human man = new Man() Human称为变量的静态类型(Static Type),或者叫外观类型(Apparent Type),后面的Man称为变量的实际类型(Actual Type);
        • 静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期间可知的; 而实际类型变化的结果在运行期才可确定,编译器在编译时并不知道一个对象的实际类型是什么;
      • 编译器在重载选择方法时,通过参数的静态类型而不是实际类型作为判定依据的;
      • 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,典型应用是方法重载(Overload); 静态分派发生在编译阶段,因此确定静态分派的动作实际上不是jvm执行的;
    • 动态分派 [运行阶段jvm的选择过程]
      • 重写(Override) ,可通过字节码指令invokevirtual执行多态方法,执行的第一步就是在运行期间确认接收者(将要执行方法的所有者)的实际类型,把常量池中的类方法符号引用解析到不同的直接引用上,这个过程就是java方法重写的本质;
    • 单分派和多分派
      • 方法的接收者与方法的参数统称为方法宗量;根据分派基于多少种宗量,可划分为单分派和多分派两种;
        • 单分派: 根据一个宗量对目标方法进行选择;
        • 多分派: 根据多于一个宗量对目标方法进行选择;
      • java是一门静态多分派(重载时根据调用者和参数),动态单分派(重写时根据调用者)的语言;
  • 动态类型语言支持 Dynamically Type Language

    • 动态类型语言 类型检查的主体过程是在运行期间而不是编译期(如js,python,kotlin);而编译期间进行类型检查过程的语言(java,C++)就是静态类型语言;动态语言变量无类型而变量值才有类型;
    • java.lang.invoke包支持动态编程:
      • MethodHandle;
      • MethodType :方法类型,包含方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)
      • MethodHandles.lookup() : 在指定类中查找符合给定的方法名称,方法类型,并且符合调用权限的方法句柄;
      • 与反射的区别是反射是重量级的;
    • invokedynamic 字节码指令 为了解决原有4条’invoke*'指令方法分派规则固化在jvm之中的问题,把如何查找目标方法的决定权从jvm中转嫁到具体用户代码中,让用户有更高的自由度;invokedynamic的分派逻辑不是由jvm决定的,而是由程序员决定的;
    • 使用动态语言调用父类的父类的方法: 输出为: i am grandfather;
      在这里插入图片描述

基于栈的字节码解释执行引擎

  • 解释执行,半独立编译;
  • 基于栈的指令集与基于寄存器的指令集
    • java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture),指令流中的指令大部分都是零地址指令,依赖操作数栈进行工作;而x86的二地址指令集就是基于寄存器的指令集;
    • 区别 (1+1):
      • 基于栈 iconst_1 iconst_1 iadd istore_0
        • 可移植,代码紧凑,编译器实现简单,但相同功能指令数量更多,更频繁的内存访问,执行速度慢;
      • 基于寄存器 mov eax, 1 add eax, 1
        • 性能好,实现简单

类加载及执行子系统的案例与实战

程序进行操作的主要是字节码生成类加载器这两部分的功能;

tomcat: 正统的类加载器架构

Common类加载器能加载的类都可以被Catalina和Shared使用,双方可以相互隔离;各个WebApp类加载器实例之间相互隔离;Jsp类加载器就是为了被丢弃实现HotSwap功能;

在这里插入图片描述

OSGi: 灵活的类加载器架构

Open Service Gateway Initiative 基于java语言的动态模块化规范; 运行时才能确定的网状结构;Eclipse IDE 就是OSGi的应用案例;

字节码生成技术与动态代理的实现

javac,javassist,CGLib,ASM ,Proxy.newProxyInstance;动态代理的优势实现了在原始类和接口还未知的时候,就确定了代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活的重用于不同的应用场景中;


程序编译与代码优化

  • 编译器分类:

    • 前端编译器: 将*.java转变为 *.class文件的过程; sun的javac;
    • JIT编译器(Just in Time 后端运行期编译器): 把字节码转变为机器码的过程;hotspotVm的c1,c2编译器;
    • AOT编译器 (Ahead of Time 静态提前编译器): 直接把*.java文件编译成本地机器码的过程; GCJ;
  • java语法糖:

    • 泛型与类型擦除 参数化类型的应用,也就是说所操作的数据类型被指定为一个参数;这种参数类型可以用在类,解口,方法的创建中,分别被称为泛型类,泛型接口,泛型方法;
      • java中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type 裸类型),并且在相应的地方插入了强制转型代码; 因此在运行期间的java来说,ArrayList和ArrayList就是同一个类,所以是一个语法糖;java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型;
      • Signature,LocalVariableTypeTable等新属性用于解决伴随泛型而来的参数类型的识别问题;
    • 自动装箱,拆箱,遍历循环;
      • 自动拆箱的陷阱
        在这里插入图片描述
        • Integer 内有提供的数缓存只有-128 ~ 127,超过这个范围重新创建新的空间存储这个数;所以第一个第二个为true,false;
        • == 判断两个类型的地址,在不遇到算术运算的情况下不会自动拆箱;所以第三个第四个都是返回true;
        • equals方法不处理数据转型的关系;所以第五个第六个返回true,false;
  • 注解处理器

    • 实现的注解处理器需要继承抽象类javax.annotation.processing.AbstractProcessor ,

      • 覆盖抽象方法 public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 它是javac编译器在执行注解处理器代码时要调用的过程
        • 第一个参数 获取到此注解器所要处理的注解集合;
        • 第二个参数 访问到当前这个Round中的语法树节点,每个语法树节点再这里表示为一个Element;
        • JDK 1.6 javax.lang.model 定义了16类Element
          • 包 package;
          • 枚举 enum;
          • 类 class;
          • 注解 annotation_type;
          • 接口 interface;
          • 枚举值 enum_constant;
          • 字段 field;
          • 参数 parameter;
          • 本地变量 local_variable;
          • 异常 exception_parameter;
          • 方法 method;
          • 构造函数 constructor;
          • 静态语句块 static_init
          • 实例语句块 instance_init
          • 参数化类型 type_parameter;
          • 其他语法树节点 other;
      • 常用的实例变量 protected ProcessingEnvironment processingEnv; 初始化的时候创建,代表注解处理器框架提供的一个上下文环境,要创建新的代码,向编译器输出信息,获取其他工具类等都需要这个实例变量;
    • 注解处理器除了process()方法及其参数之外,还有两个可以配合使用的Annotations

      • @SupportedAnnotationTypes 注解处理器对哪些注解感兴趣,可以使用星号*通配对所有注解都感兴趣;
      • @SupportSourceVersion 指出这个注解处理器可以处理哪些版本的java代码;
    • 每一个注解处理器在运行时都是单例的,如果不需要改变或生成语法树的内容,process()方法就可以返回一个值为false的布尔值,通知编译器这个round中的代码未发生变化,无需构造新的javaCompiler实例;

  • 晚期(运行时)优化

  • java最初是通过解释器进行解释执行的,当jvm发现某个方法或者代码块执行频繁,就会把这些代码认定为’热点代码(HotSpot)’,提高热点代码的效率,运行时,jvm将会把这些代码编译成与本地平台相关的机器码,并进行层次的优化,完成这个任务的编译器为即时编译器(Jit)

  • java hotspot jvm是解释器与编译器并存的架构, 当程序需要快速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行;当程序运行后,随着时间得推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取较高的执行效率;

  • hotspot jvm内置两个即时编译器,分别称为 Client Compiler,Server Compiler 简称 C1编译器,C2编译器; 分别分为 混合模式,编译模式,解释模式;

  • 编译优化技术 (太复杂,选几点记录一下)

    • 方法内联 (Method Inlining) 去除方法调用的成本(如建立栈帧);为其他优化建立良好的基础;非虚方法可以直接内联;
    • 冗余访问消除 ,公共子表达式消除;
    • 复写传播
    • 无用代码消除;
    • 逃逸分析(如果一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,可能进行一些高效的优化(是否说明少用形式参数?))

这块挺复杂的,只是粗浅的看了下,有兴趣的可以看原书;


发布了46 篇原创文章 · 获赞 3 · 访问量 5092
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章