运行时栈帧结构
虚拟机字节码执行引擎:有多种实现方式,有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)
栈帧(Stack Frame)
虚拟机进行方法调用和执行的结构
存储了局部变量表、操作数栈、动态连接和方法返回地址(在编译代码的时候就已经确定了其大小)
局部变量表
局部变量表十一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
在java被编译为class文件时,在Code属性的max_locals数据项确定该方法的最大局部变量容量
slot变量槽是最小单位,32位以内的数据类型
方法执行时虚拟机使用局部变量表完成参数值到参数变量列表的传递过程
slot是可以复用的,所以会出现一个方法体的内存如没被覆盖,gc回收无响应
局部变量没有赋初始值是不能使用的,系统在准备阶段是没有给默认值的
操作数栈
在java被编译为class文件时,在Code属性的max_stacks数据项确定了操作栈的最大深度
操作数栈中元素的数据类型必须与字节码执行的序列严格匹配
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
一部分引用会在类加载阶段或第一次使用的时候直接转化为直接引用,称为静态解析
一部分在每一次运行期间转化为直接引用
方法返回地址
一种执行引擎遇到任意一个方法返回的字节码指令
一种是执行中遇到异常
附加信息
虚拟机允许增加一些规范里没有描述的信息到栈帧中,例如调试相关的信息
方法调用
需要在类加载期间甚至运行期间才能确定目标方法的直接引用,在此之前都只是符号引用
解析
方法在程序真正运行之前就有一个可确定的调用版本,并且这个版本在运行期间不可改变
“编译期可知,运行期不可变”:
静态方法和、私有方法、实例构造器、父类方法适合在类加载阶段解析(invokestatic、invokespecial)
虚方法、接口方法(invokevirtual、invokeinterface)
分派
分派调用过程将揭示多态性特征的一些最基本体现(如:“”重载、重写“”)
静态分派
所有依赖静态类型来定位方法执行版本的分派动作都称为静态分派
发生在编译阶段,不是由虚拟机来执行的
与重载有很大关系。static
动态分配
与重写有很大关系。abstract
单分派与多分派
java语言是静态多分配、动态单分配
虚拟机动态分派的实现
在方法区建立一个虚方法表、接口有接口方法表
还有:内联缓存、基于“类继承关系分析”技术的守护内联
基于栈的字节码解释执行引擎
基于栈的指令集与基于寄存器的指令集
java基本上都基于栈,优点:可移植性,缺点:执行速度稍慢些
基于栈解释器执行过程:
Tomcat:正统的类加载器架构
主流的java web服务器,如:Tomcat、jetty、weblogic、WebSphere等都实现了自定义的类加载器
1.部署在同一个服务器上的两个web应用使用的java类库可以实现相互隔离
2.部署在同一个服务器上的两个web应用所使用的java类库可以相互共享,例如spring
3.服务器保证自身的安全不受部署的web应用程序影响,一般服务器所使用的类库与应用隔离
4.支持JSP应用的web服务器,十有八九需要支持hotSwap功能
Tomcat目录为例:
/common/*:类库可被Tomcat和所有web应用程序使用
/server/*:类库可被Tomcat使用,对所有的web应用程序都不可见
/shared/*:类库可被所有的web应用程序共同使用,对Tomcat不可见
/WEB-INF/*:类库仅仅可以被此web应用程序使用,对Tomcat和其他web应用不可见
对于Tomcat6.x版本,只有配置才会真正建立CatalinaClassLoader和sharedClassLoader,没有的话都使用CommonClassLoader代替,所以/common /server /shared默认合并成一个/lib目录
OSGi:灵活类加载架构:
字节码生成技术与动态代理实现:
统一调用了InvocationHandler对象的invoke()方法
Retrotranslator:跨越JDK版本
Java逆向移植 工具(Java backporting tools),想搞这些需要对每个JDK新增功能十分了解
编译器层面的改进。如自动装箱之类
对Java Api的代码增强。如Java并发包
下面这俩无能为力:
在字节码中进行支持的改动。如JDK1.7中的语法特性,动态语言支持
虚拟机内部的改进。如JDK1.5中重新定义了java的内存模型、CMS收集器
实战:自己动手实现远程执行功能
排查问题,查看内存中的一些参数值,没有办法把这些值输出到界面或者日志中,又或者想查看缓存的一些数据
目标:
不依赖JDK版本
不改变原有服务端程序部署,不依赖第三方库
不侵入原有程序
考虑到BeanShell script或java script脚本不方便
临时代码应该具备充分的自由度
临时代码的执行结果能返回到客户端,包含程序中输出信息及抛出的异常
思路:
如何编译提交到服务器的代码
使用tools.jar包(有外部依赖)
如何执行编译之后的java代码
使用反射调用某个方法(不实现任何接口,借用Java人人皆知的main()),提交的执行后应当能被卸载和回收掉
如何收集java代码的执行结果,包含异常
直接在执行的类中把对system.out的符号引用替换为我们准备的printstream符号引用
实现:
1.使用HotSwapClassLoader,公开父类中的protected方法的defineClass()
2.使用ClassModifier,将java.lang.System替换为我们自己定义的HackSystem类,修改常量池引用字符串
3.使用HackSystem,将out和err变为printStream的输出对象
4.使用JavaClassExecuter,发起一连串任务
验证:
早期(编译器优化)
前端编译器:java编译为class
JIT后端运行编译器:字节码编译为机器码
AOT静态提前编译器:直接将java编译为机器码
前端编译器javac:
解析与符号填充
插入式注解处理器的注解处理过程
分析与字节码生成过程
解析与填充符号表:
词法分析:将源代码字符流转变为标记集合
语法分析:根据标记(Token)集合来构造抽象语法树的过程
填充符号表:符号表是由一组符号地址和符号信息构成的表格(在编译器任何时期都要用到)
在语义分析中符号表用于语义检查
在目标代码生成时用于地址分配的依据
注解处理器:
语义分析与字节码生成:
标注检查
数据及控制流分析
解语法糖
字节码生成:String字符串的加操作就在这一步
Java语法糖的味道
泛型与类型擦除
本质是参数化类型的应用,泛型技术实际上是Java的一颗语法糖
自动装箱、拆箱与循环遍历
包装类的“==”运算在没有遇到算数运算的情况下不会自动拆箱,而且它们的equals方法不会处理数据类型转换
条件编译
java会根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉
除以上,还有:内部类、枚举类、断言语句、对枚举和字符串的switch支持、try语句中定义和关闭资源
实战:插入式注解处理器
目标:
通过注释处理器检查驼峰命名法
代码实现:
。。。
相似的还有:hibernate标签校验、Lombok自动为字段生成getter和setter(根据已有元素生成新的语法树元素)
晚期(运行期)优化
Hotspot虚拟即时编译器(JIT,与虚拟机种类密切相关)
解释器与编译器:
程序需要迅速启动和执行的时候,运行环境内存资源限制时候,解释器
随着时间推移”热点代码“,将代码编译成本地代码,编译器(多层次编译:平衡启动和运行)
编译对象与触发条件:
多次调用的代码,多次执行的循环体
热点探测:采样热点探测、基于计数器热点探测、基于”踪迹“的热点探测(-XX:CompileThreshold)
热度衰减:在GC的时候顺带进行(回边计数器没有热度衰减)
编译过程:
编译优化技术
公共子表达是消除
数组范围检查消除
方法内联
逃逸分析:栈上分配、同步消除、标量替换
Java与C/C++编译器比对