Java 是一个解释型语言(使用了JIT后, 也有变成本地机器码的, 但一般意义上都是先预编译成字节码, 解释执行),那字节码里面包含了那些?解释的过程如何?
1、Class 字节码
I、基本信息
Class识别、版本信息
II、常量池
类似TLV表述的结构,数据类型主要是用 U1、U2 ... (对应 1,2,4,8个字节)的无符号数。
常量池并不是指类里面的常量,而是类里面所有名称和限定符(字面常量、编译常量)
因为,class中用来描述常量长度是 u2类型,即最大只能描述65535,所以java中变量名最大只能到 65535
常用的特殊类型
对象类型是用 字符 L
数组类型使用 [
比如 java.lang.String[] 则表示为[Ljava/lang/String
III、各种表集合
字段表 - 域变量 (类型、操作指令、行数等)
方法表 - 类方法 (类型、操作指令、行数等)
指令用u1 类型来标识,那说明最多智能有 256条指令;code_length使用 u4 类型,理论上可以有4294967295条指令,实际上java只允许最多 65535条指令,否则编译失败(以后不知道会否放开),因为这个限制在一些超长的方法(比如复杂jsp编译后的java),会编译失败
如果子类没有重写父类的字段和方法,就不会在子类的class文件中出现父类的参数和方法。
javap -verbose XXXClass 可以反编字节码
2、Class 加载和解析
I、加载过程
字节码的加载流程: 装载 - 链接(验证 - 准备 - 解析) - 初始化
装载(load):查找并加载二进制数据
链接(Link)- 在这一阶段,主要是 验证:确保加载类的正确性;准备:为静态变量分配内存,并初始化默认值;解析:把类的符号引用转换成直接引用
初始化:类的初始化发生在(I、创建类实例new;II、调用类的静态方法和变量;III、反射调用class.forName;IV、初始化子类;V、JVM启动时标明的启动类)
II、加载机制
Java的 类加载机制:双亲委派模型
Java的类加载是一个委托模型,三级加载,双亲委派模式
System(系统) – Extension(扩展) – Bootstrap(引导) 当System加载器碰到一个类时,首先委托Extension,委托Bootstrap加载,如果无法加载,才自己加载
这样有利于类的层次,也避免恶意破坏
两个类是否相同?
java判断是否是同一个类,除了检查类的名称(全路径),同时还判断该类的类加载器是否相同,如果被加载到不同的类加载器,类也是不同的。所以核心类都必须是引导加载
III、双亲委派模型的缺陷 - 机制的补充
这种单向委派的机制,对于框架设计有个严重的问题, 框架只设计接口, 实现由具体应用完成。
现在,子类加载器加载的类 知道父类,父类加载器加载的类不知道子类, 这样就无法完成闭环。
比如a、Java规范提供了很多接口,比如JDBC,需要具体的厂商来实现,这时候加载;b、典型的Java Servlet规范,要求每个Web应用使用自己的加载类
解决方法就是,线程上下文加载器;
通过类java.lang.Thread getContextClassLoader()和setContextClassLoader() 来获取和设置线程的上下文加载器。创建线程时, 如果未设置上下文加载器,从父线程继承一个线程上下文加载器,默认的是 系统 上下文加载器
这样 父类加载器 发现无法加载类时,可以通过 获取线程上下文加载器(即子类加载器),请求子类加载器去加载代码
线程上下文加载器 VS 当前加载器 如何选
a、默认使用当前加载器,除非有 父依赖子的情况
b、更好的策略 是 当前加载器和线程上下文加载器谁是子,就使用谁
何时需要关注类加载器?
当我们使用 反射\动态代理 等需要加载类功能的时候,背后就是在使用类加载器。特别是Web应用,比如tomcat,每个web应用都有一个对应的类加载器实例。该类加载器首先会加载应用,找不到再代理给父类加载器。这与一般类加载器是反的,也是 Java Servlet 规范推荐的,目的是Web应用的优先级高于Web容器的优先级(不加载核心库)
tomcat的classloader怎么隔离类
Tomcat的类加载机制
JDK默认的加载器
common类加载器 - CommonClassLoader (公用部分)
Catalina类加载器 CatalinaClassLoader (服务器部分) Shared类加载器 SharedClassLoader(应用部分)
WebApp类加载器 WebappClassLoader
Jsp类加载器 JsperLoader
其中CatinaClassLoader用来加载 tomcat自己的类,而WebAppClassLoader每个应用都有一个自己的类加载器
在Context容器(对应一个web应用)启动时,就会启动一个WebAppClassLoader,所以每个应用互不干涉
IV、如何自定义 类加载器
最佳的方式,利用已有的加载器,做一些扩展,比如 URL 的加载器 URLClassLoader
或者自己实现 ClassLoader类,重点是 实现 findClass 读取字节码,并调用 defineClass转换为Class 对象
V、加载即其他
利用类加载的灵活机制,有很多衍生的技术
动态代理
asm
cglib
OSGi
...
如果加载的类是确定的,当类特别多的话,如果需要加快加载时间,可以关闭加载验证过程 –Xverify:none;
VI、初始化顺序的问题
a、类加载时, 首先加载静态变量和静态方法
b、因为子类不持有父类的 属性,所以直接通过子类引用父类静态属性,不会触发子类初始化
c、构成初始化的几个条件,比如new
父类静态 > 子类静态 > 父类构造 > 子类构造
即加载(静态)优先, 长辈优先
3、Class执行 - 多态实现的关键
有些方法(静态方法、私有方法等等)在解析时,就能确定方法的入口指针(直接引用),但其他方法, 可能存在override, 还有 overload的一些情况。
public class Dispatch {
static class QQ{}
static class _360{}
public static class Father{
public void hardChoice(QQ arg){
System.out.println("father choose qq");
}
public void hardChoice(_360 arg){
System.out.println("father choose 360");
}
}
public static class Son extends Father{
public void hardChoice(QQ arg){
System.out.println("son choose qq");
}
public void hardChoice(_360 arg){
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
public static void main(java.lang.String[]);
Code:
Stack=3, Locals=3, Args_size=1
0: new #16; //class ch08/Dispatch$Father
3: dup
4: invokespecial #18; //Method ch08/Dispatch$Father."":()V
7: astore_1
8: new #19; //class ch08/Dispatch$Son
11: dup
12: invokespecial #21; //Method ch08/Dispatch$Son."":()V
15: astore_2
16: aload_1
17: new #22; //class ch08/Dispatch$_360
20: dup
21: invokespecial #24; //Method ch08/Dispatch$_360."":()V
24:invokevirtual #25; //Method ch08/Dispatch$Father.hardChoice:(Lch08/Dispatch$_360;)V
27: aload_2
28: new #29; //class ch08/Dispatch$QQ
31: dup
32: invokespecial #31; //Method ch08/Dispatch$QQ."":()V
35:invokevirtual #32; //Method ch08/Dispatch$Father.hardChoice:(Lch08/Dispatch$QQ;)V
38: return
LineNumberTable:
line 30: 0
line 31: 8
line 32: 16
line 33: 27
line 34: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 args [Ljava/lang/String;
8 31 1 father Lch08/Dispatch$Father;
16 23 2 son Lch08/Dispatch$Father;
}
overload通过指定参数类型,编译时指向时有对应的参数类型,执行时通过不同的参数类型,就能找到具体的方法,称为静态分派
override编译出来的类型,都是相同的 类型, JVM 在运行时提供一个虚拟方法表, 如果子类改写了父类,则方法入口地址为子类方法地址,这样就能识别指向查找的方法了, 称为动态分派