- 1、Java 内存区域与内存溢出
- 2、垃圾回收与内存分配
- 3、类文件结构
- 4、类加载
- 5、字节码执行引擎
1、Java 内存区域与内存溢出
1.1、运行时数据区域
参考:JVM%20规范,Memories%20of%20a%20Java%20Runtime堆:JVM%20启动时按-Xmx,-Xms大小创建的内存区域,用于分配对象、数组所需内存,由%20GC%20管理和回收方法区:存储被%20JVM%20加载的类信息(字段、成员方法的字节码指令等)、运行时常量池(字面量、符号引用等)、JIT%20编译后的%20Code%20Cache%20等信息;JDK8%20前%20Hotspot%20将方法区存储于永久代堆内存,之后参考%20JRockit%20废弃了永久代,存储于本地内存的%20Metaspace%20区直接内存:JDK1.4%20引入%20NIO%20使用%20Native/Unsafe%20库直接分配系统内存,使用%20Buffer,Channel%20与其交互,避免在系统内存与%20JVM%20堆内存之间拷贝的开销线程私有内存
- 程序计数器:记录当前线程待执行的下一条指令位置,上下文切换后恢复执行,由字节码解释器负责更新
- JVM%20栈:
- 描述%20Java%20方法执行的内存模型:执行新方法时创建栈帧,存储局部变量表、操作数栈等信息
- 存储单位:变量槽%20slot,long,double占%202%20个%20slot,其他基本数据类型、引用类型占%201%20个,故表的总长度在编译期可知
- 本地方法栈:执行本地%20C/C++%20方法
1.2、JVM%20对象
1.%20创建对象
分配堆内存:类加载完毕后,其对象所需内存大小是确定的;堆内存由多线程共享,若并发创建对象都通过%20CAS%20乐观锁争夺内存,则效率低。故线程创建时在堆内存为其分配私有的分配缓冲区(TLAB:Thread%20Local%20Allocation%20Buffer)
- 内存模型
- 分配流程
注:当%20TLAB%20剩余空间不足以分配新对象,但又小于最大浪费空间阈值时,才会加锁创建新的%20TLAB零值初始化对象的堆内存、设置对象头信息、执行构造函数%20
()V
2.%20对象的内存布局
对象头
- Mark%20Word:记录对象的运行时信息,如%20hashCode,GC%20分代年龄,尾部%202%20bit%20用于标记锁状态
- Class%20Pointer:指向所属的类信息
- 数组长度(可选,对象为数组):4%20字节存储其长度
对象数据:各种字段的值,按宽度分类紧邻存储对齐填充:内存对齐为%201%20个字长整数倍,减少%20CPU%20总线周期验证:openjdk/jol%20检查对象内存布局
public%20class%20User%20{%20%20%20%20private%20int%20age%20=%20-1;%20%20%20%20private%20String%20name%20=%20"unknown";}
//%20java%20-jar%20~/Downloads/jol-cli-latest.jar%20internals%20-cp%20.%20com.jol.UserOFF%20%20SZ%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20TYPE%20DESCRIPTION%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20VALUE0%20%20%208%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(object%20header:%20mark)%20%20%20%20%200x0000000000000001%20(non-biasable;%20age:%200)8%20%20%204%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(object%20header:%20class)%20%20%20%200xf8021e85%20//%20User.class%20引用地址%2012%20%20%204%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int%20User.age%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20-1%20%20%20%20%20%20%20%20%20//%20基本类型则直接存储值%2016%20%20%204%20%20%20java.lang.String%20User.name%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(object)%20%20%20//%20引用类型,指向运行时常量池中的%20String%20对象%2020%20%20%204%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(object%20alignment%20gap)%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20有%204%20字节的内存填充Instance%20size:%2024%20bytes
1.3、内存溢出
堆内存:-Xms指定堆初始大小,当大量无法被回收的对象所占内存超出-Xmx上限时,将发生内存溢出%20OutOfMemoryError
- 排查:通过%20Eclipse%20MAT%20分析%20
-XX:+HeapDumpOnOutOfMemory生成的%20*.hprof%20堆转储文件,定位无法被回收的大对象,找出其%20GC%20Root%20引用路径 - 解决:若为内存泄露,则修改代码用null显式赋值、虚引用等方式及时回收大对象;若为内存溢出,大对象都是必须存活的,则调大
-Xmx、减少大对象的生命周期、检查数据结构使用是否合理等
分析%20GC%20Root%20发现com.ch02.HeapOOM对象间接引用了大量的//%20-Xms20m%20-Xmx20m%20-XX:+HeapDumpOnOutOfMemoryErrorpublic%20class%20HeapOOM%20{%20%20static%20class%20OOMObject%20{}%20%20public%20static%20void%20main(String[]%20args)%20{%20%20%20%20%20%20List<OOMObject>%20vs%20=%20new%20ArrayList<>();%20%20%20%20%20%20while%20(true)%20%20%20%20%20%20%20%20%20%20vs.add(new%20OOMObject());%20%20}}
OOMObject对象,共占用%2015.4MB%20堆内存,无法回收最终导致%20OOM栈内存:
-Xss指定栈大小,当栈深度超阈值(比如未触发终止条件的递归调用)、本地方法变量表过大等,都可能导致内存溢出%20StackOverflowError方法区:-XX:MetaspaceSize指定元空间初始大小,-XX:MaxMetaspaceSize指定最大大小,默认%20-1%20无限制,若在运行时动态生成大量的类,则可能触发%20OOM运行时常量池:strObj.intern()动态地将首次出现的字符串对象放入字符串常量池并返回,JDK7%20前会拷贝到永久代,之后则直接引用堆对象
直接内存:String%20s1%20=%20"java";%20//%20类加载时,从字节码常量池中拷贝符号到了运行时常量池,在解析阶段初始化的字符串对象String%20s2%20=%20"j";String%20s3%20=%20s2%20+%20"ava";%20//%20堆上动态分配的字符串对象println(s3%20==%20s1);%20%20%20%20%20%20%20%20%20%20//%20falseprintln(s3.intern()%20==%20s1);%20//%20true%20//%20已在字符串常量池中存在
-XX:MaxDirectMemorySize指定大小,默认与-Xmx一样大,不被%20GC%20管理,申请内存超阈值时%20OOM
2、垃圾回收与内存分配
GC%20可分解为%203%20个子问题:which(哪些内存可被回收)、when(什么时候回收)、how(如何回收)
2.1、GC%20条件
1.%20引用计数算法(reference%20counting)
原理:每个对象都维护一个引用计数器rc,当通过赋值、传参等方式引用它时rc++,当引用变量修改指向、离开函数作用域等方式解除引用时rc—,递减到%200%20时说明对象无法再被使用,可回收。伪代码:
assign(var,%20obj):incr_ref(obj)%20#%20self%20=%20self%20#%20先增再减,避免引用自身导致内存提前释放decr_ref(var)var%20=%20objincr(obj):obj.rc++decr(obj):obj.rc--if%20obj.rc%20==%200:%20%20remove_ref(obj)%20#%20断开%20obj%20与其他对象的引用关系%20%20gc(obj)%20%20%20%20%20%20%20%20%20#%20回收%20obj%20内存
优点:思路简单,对象无用即回收,延迟低,适合内存少的场景缺点:此算法中对象是孤立的,无法在全局视角检查对象的真实有效性,循环引用的双方对象需引入外部机制来检测和回收,如下图红色圈(图源:what-is-garbage-collection)
2.%20可达性分析算法(reachability%20analysis)
原理:从肯定不会被回收的对象(GC%20Roots)出发,向外搜索全局对象图,不可达的对象即无法再被使用,可回收;常见可作为%20GC%20Root%20的对象有:
- 执行上下文:JVM%20栈中参数、局部变量、临时变量等引用的堆对象
- 全局引用:方法区中类的静态引用、常量引用(如%20StringTable%20中的字符串对象)所指向的对象
优点:无需对象维护%20GC%20元信息,开销小;单次扫描即可批量识别、回收对象,吞吐高缺点:多线程环境下对象间的引用关系随时在变化,为保证%20GC%20Root%20标记的准确性,需在不变化的%20snapshot%20中进行,会产生%20Stop%20The%20World(以下简称%20STW)%20卡顿现象
3.%20四种引用类型
| 引用类型 | 类 | 回收时机 |
|---|---|---|
| 强引用 | - | 只要与%20GC%20Root%20存在引用链,则不被回收 |
| 软引用 | SoftReference | 只被软引用所引用的对象,当%20GC%20后内存依然不足,才被回收 |
| 弱引用 | WeakReference | 只被弱引用所引用的对象,无论内存是否足够,都将被回收 |
| 虚引用 | PhantomReference | 被引用的对象无感知,进行正常%20GC,仅在回收时通知虚引用(回调) |
示例:限制堆內存%2050MB,其中新生代%2030MB,老年代%2020MB;依次分配%205%20次%2010MB%20的byte[]对象,仅使用软引用来引用,观察%20GC%20过程
public%20static%20void%20main(String[]%20args)%20{%20%20%20%20//%20softRefList%20-->%20SoftReference%20-->%2010MB%20byte[]%20%20%20%20%20List<SoftReference<byte[]>>%20softRefList%20=%20new%20ArrayList<>();%20%20%20%20ReferenceQueue<byte[]>%20softRefQueue%20=%20new%20ReferenceQueue<>();%20//%20无效引用队列%20%20%20%20for%20(int%20i%20=%200;%20i%20<%205;%20i++)%20{%20%20%20%20%20%20%20%20SoftReference<byte[]>%20softRef%20=%20new%20SoftReference<>(new%20byte[10*1024*1024],%20softRefQueue);%20%20%20%20%20%20%20%20softRefList.add(softRef);%20%20%20%20%20%20%20%20for%20(SoftReference<byte[]>%20ref%20:%20softRefList)%20//%20dump%20所有软引用指向的对象,检查是否已被回收%20%20%20%20%20%20%20%20%20%20%20%20System.out.print(ref.get()%20==%20null%20?%20"gced%20"%20:%20"ok%20");%20%20%20%20%20%20%20%20System.out.println();%20%20%20%20}%20%20%20%20Reference<?%20extends%20byte[]>%20ref%20=%20softRefQueue.poll();%20%20%20%20while%20(ref%20!=%20null)%20{%20%20%20%20%20%20%20%20softRefList.remove(ref);%20//%20解除对软引用对象本身的引用%20%20%20%20%20%20%20%20ref%20=%20softRefQueue.poll();%20%20%20%20}%20%20%20%20System.out.println("effective%20soft%20ref:%20"%20+%20softRefList.size());%20//%202}
//%20java%20-verbose:gc%20-XX:NewSize=30m%20-Xms50m%20-Xmx50m%20-XX:+PrintGCDetails%20com.ch02.DemoRefok%20ok%20ok%20//%20分配第三个%20[]byte%20时,Eden%20GC%20无效,触发%20Full%20GC%20将一个%20[]byte%20晋升到老年区//%20此时三个%20byte[]%20都只被软引用所引用,被标记为待二次回收(若为弱引用,此时%20Eden%20已被回收)[GC%20(Allocation%20Failure)%20--[PSYoungGen:%2021893K->21893K(27136K)]%2021893K->32141K(47616K),%200.0046324%20secs][Full%20GC%20(Ergonomics)%20[PSYoungGen:%2021893K->10527K(27136K)]%20[ParOldGen:%2010248K->10240K(20480K)]%2032141K->20767K(47616K),%20[Metaspace:%202784K->2784K(1056768K)],%200.004%20secs]ok%20ok%20ok//%20再次%20GC,前三个%20byte[]%20全部被回收[GC%20(Allocation%20Failure)%20--[PSYoungGen:%2020767K->20767K(27136K)]%2031007K->31007K(47616K),%200.0007963%20secs][Full%20GC%20(Ergonomics)%20[PSYoungGen:%2020767K->20759K(27136K)]%20[ParOldGen:%2010240K->10240K(20480K)]%2031007K->30999K(47616K),%20[Metaspace:%202784K->2784K(1056768K)],%200.003%20secs][GC%20(Allocation%20Failure)%20--[PSYoungGen:%2020759K->20759K(27136K)]%2030999K->30999K(47616K),%200.0007111%20secs][Full%20GC%20(Allocation%20Failure)%20[PSYoungGen:%2020759K->0K(27136K)]%20[ParOldGen:%2010240K->267K(20480K)]%2030999K->267K(47616K),%20[Metaspace:%202784K->2784K(1056768K)],%200.003%20secs]gced%20gced%20gced%20okgced%20gced%20gced%20ok%20ok
4.%20finalize
原理:若对象不可达,被标记为可回收后,会进行finalize()是否被重写、是否已执行过等条件筛选,若通过则对象会被放入%20F-Queue%20队列,等待低优先级的后台%20Finalizer%20线程触发其finallize()%20的执行(不保证执行结束),对象可在finalize中建立与%20GC%20Root%20对象图上任一节点的引用关系,来逃脱%20GC使用:finalize%20机制与%20C++%20中的析构函数并不等价,其执行结果并不确定,不推荐使用,可用try-finally替代
2.2、GC%20算法
分代收集理论
两个分代假说:符合大多数程序运行的实际情况
- 弱分代假说:绝大多数对象是朝生夕灭,生存时间极短
- 强分代假说:熬过越多次%20GC%20的对象,越可能被继续使用,越难以回收
对应地,JVM%20堆被划分为%202%20个不同区域,将对象按年龄分类,兼顾了%20GC%20耗时与内存利用率
- 新生代:大量对象将被回收,只关注仍存活的对象,逐步晋升
- 老年代:大量对象不被回收,只关注要被回收的对象
跨代引用
- 问题:老年代会引用新生代,新生代%20GC%20时需遍历老年代中大量的存活对象,分析可达性,时间复杂度高
- 背景:相互引用的对象倾向于同时存亡,比如跨代引用关系中的新生代必然会逐步晋升,最终消除跨代关系
- 假说:跨代引用相比同代引用只占极少数,无需全量扫描老年代
- 实现:新生代维护全局数据结构:记忆集(Remembered%20Set),将老年代分为多个子块,标记存在跨代引用的子块,等待后续扫描;代价:为保证记忆集的正确性,需在跨代引用建立或断开时保持同步
2.3、标记清除:Mark-Sweep
- 原理:标记不可达对象,统一清理回收,反之亦可
- 缺点:执行效率不稳定,回收耗时取决于活跃对象的数量;内存碎片多,会出现内存充足但无法分配过大的连续内存(数组)
2.4、标记复制:Mark-Copy
- 理论:将堆内存切为两等份%20A,%20B,每次仅使用%20A,用完后标记存活对象复制到%20B,清空%20A%20后执行%20swap
- 优点:直接针对半区回收,无内存碎片问题;分配内存只需移动堆顶指针,高效顺序分配
- 缺点:当%20A%20区有大量存活对象时,复制开销大;B%20区长时间闲置,内存浪费严重
- 实践:对于存活对象少的新生代,无需按%201:1%20分配,而是按%208:1:1%20的内存布局,其中%20Eden%20和%20From%20区同时使用,只有%20To%20区会被闲置(担保机制:若%20To%20区不够容纳%20Minor%20GC%20后的存活对象,则晋升到老年区)
2.5、标记整理:Mark-Compact
- 原理:标记存活对象后统一移动到内存空间一侧,再回收边界之外的内存
- 优点:内存模型简单,无内存碎片,降低内存分配和访问的时间成本,能提高吞吐
- 缺点:对象移动需%20STW%20同步更新引用关系,会增加延迟
2.6、HotSpot%20GC%20算法细节
发起%20GC:安全点与安全区域
- 问题:为保证可达性分析结果的准确性,需挂起用户线程(STW),再从各线程的执行上下文中收集%20GC%20Root,如何通知线程挂起?
- 安全点:HotSpot%20内部有线程中断标记;在各线程的方法调用、循环跳转、异常跳转等会长时间执行的指令处,额外插入检查该标记的test高效指令;若轮询发现标记为真,线程会主动在最近的%20SafePoint%20处挂起,此时其栈上对象的引用关系不再变化,可收集%20GC%20Root%20对象
- 安全区域:引用关系不会变化的指令区域,可安全地收集%20GC%20Root;线程离开此区域时,若%20GC%20Root%20收集过程还未结束,则需等待
加速%20GC:CardTable
问题:非收集区域(老年代)会存在到收集区域(新生代)的跨代引用,如何避免对前者的全量扫描?卡表:记忆集的字节数组实现;将老年代内存划分为%20Card%20Page(512KB)大小的子内存块,若新建跨代引用,则将对应的%20Card%20标记为%20dirty,GC%20时只需扫描老年代中被标记为%20dirty%20的子内存块写屏障:有别于
volatile禁用指令重排的内存屏障,GC%20中的写屏障是在对象引用更新时执行额外%20hook%20动作的机制。简单实现:
void%20oop_field_store(oop*%20field,%20oop%20new_val)%20{%20//%20oop:%20ordinary%20object%20pointer%20%20%20%20//%20pre_write_barrier(field,%20new_val);%20//%20写前屏障:更新前先执行,使用%20oop%20旧状态%20%20%20%20*field%20=%20new_val;%20%20%20%20post_write_barrier(field,%20new_val);%20//%20写后屏障:更新完才执行}
使用写屏障保证%20CardTable%20的实时更新(图源:The%20JVM%20Write%20Barrier%20-%20Card%20Marking)
正确%20GC:并发可达性分析
参考演讲:Shenandoah:%20The%20Garbage%20Collector%20That%20Could%20by%20Aleksey%20Shipilev问题:GC%20Roots%20的对象源固定,故枚举时%20STW%20时间短暂且可控。但后续可达性分析的时间复杂度与堆中对象数量成正相关,即堆中对象越多,对象图越复杂,堆变大后%20STW%20时间不可接受解决:并发标记。引出新问题:用户线程动态建立、解除引用,标记过程中图结构发生变化,结果不可靠;证明:用三色法描述对象状态
- 白色:未被回收器访问过的对象;分析开始都是白色,分析结束还是白色则不可达
- 灰色:被回收器访问过,但其上至少还有%201%20个引用未被扫描(中间态)
- 黑色:被回收器访问过,其上引用全部都已被扫描,存在引用链,为存活对象;若其他对象引用了黑色对象,则不必再扫描,肯定也存活;黑色不可能直接引用白色
STW%20无并发的正确标记:顶部%203%20个对象将被回收用户线程并发修改引用,会导致标记结果无效,分%202%20种情况:
- 少回收,对象标记为存活,但用户解除了引用:产生浮动垃圾,可接受,等待下次%20GC
- 误回收,对象标记为可回收,但用户新建了引用:实际存活对象被回收,内存错误
论文《Uniprocessor%20Garbage%20Collection%20Techniques%20-%20Paul%20R.%20Wilson》§3.2%20证明了「实际存活的对象被标记为可回收」必须同时满足两个条件(有时间序)
- 插入一条或多条从黑色到白色的新引用
- 删除所有灰色到该白色的直接、间接引用
为正确实现标记,打破其中一个条件即可(类比打破死锁四个条件之一的思想),分别对应两种方案:
- 增量更新%20Increment%20Update:记录黑到白的引用关系,并发标记结束后,以黑为根,重新扫描;A%20直接存活
- 原始快照%20SATB(Snapshot%20At%20The%20Begining):记录灰到白的解引用关系,并发标记结束后,以灰为根,重新扫描;B%20为灰色,最后变为黑色,存活。需注意,若没有步骤%203,则%20B,C%20变为浮动垃圾
2.7、经典垃圾回收器
Serial,%20SerialOld
原理:内存不足触发%20GC%20后会暂停所有用户线程,单线程地在新生代中标记复制,在老年代中标记整理,收集完毕后恢复用户线程优点:全程%20STW%20简单高效缺点:STW%20时长与堆对象数量成正相关,且%20GC%20线程只能用到%201%20core%20无法加速场景:单核%20CPU%20且可用内存少(如百兆级),JDK1.3%20之前的唯一选择
ParNew
原理:多线程并行版的%20Serial%20实现,能有效减少%20STW%20时长;线程数默认与核数相同,可配置场景:JDK7%20之前搭配老年代的%20CMS%20回收器使用
Parallel,%20Parallel%20Old
垃圾回收有两个通常不可兼得的目标
- 低延迟:STW%20时长短,响应快;允许高频、短暂%20GC,比如调小新生代空间,加快收集延迟(吞吐下降)
- 高吞吐量:用户线程耗时%20/(用户线程耗时%20+%20GC%20线程耗时)高,GC%20总时间低;允许低频、单次长时间%20GC,(延迟增加)
原理:与%20ParNew%20类似都是并行回收,主要增加了%203%20个选项(倾向于提高吞吐量)
-XX:MaxGCPauseTime:控制最大延迟-XX:GCTimeRatio:控制吞吐(默认%2099%)-XX:+UseAdaptiveSizePolicy%20:启用自适应策略,自动调整%20Eden%20与%202%20个%20Survivor%20区的内存占比-XX:SurvivorRatio,老年代晋升阈值%20-XX:PretenureSizeThreshold
CMS
CMS:Concurrent%20Mark%20Sweep,即并发标记清除,主要有%204%20个阶段
- 初始标记(initial%20mark):STW%20快速收集%20GC%20Roots
- 并发标记(concurrent%20mark):从%20GC%20Roots%20出发检测引用链,标记可回收对象;与用户线程并发执行,通过增量更新来避免误回收
- 重新标记(remark):STW%20重新分析被增量更新所收集的%20GC%20Roots
- 并发清除(concurrent%20sweep):并发清除可回收对象
优点:两次%20STW%20时间相比并发标记耗时要短得多,相比前三种收集器,延迟大幅降低缺点
- CPU%20敏感:若核数较少(<%204core),并发标记将占用大量%20CPU%20时间,会导致吞吐突降
- 无法处理浮动垃圾:
-XX:CMSInitiatingOccupancyFration(默认%2092%)指定触发%20CMS%20GC%20的阈值;在并发标记、并发清理的同时,用户线程会产生浮动垃圾(引用可回收对象、产生新对象),若浮动垃圾占比超过-XX:CMSInitiatingOccupancyFration;若%20GC%20的同时产生过多的浮动垃圾,导致老年代内存不足,会出现%20CMS%20并发失败,退化为%20Serial%20Old%20执行%20Full%20GC,会导致延迟突增 - 无法避免内存碎片:
-XX:CMSFullGCsBeforeCompaction(默认%200)指定每次在%20Full%20GC%20前,先整理老年代的内存碎片G1
特点:基于%20region%20内存布局实现局部回收;GC%20延迟目标可配置;无内存碎片问题
|%20
| G1 | 之前回收器 | |
|---|---|---|
| 堆内存划分方式 | 多个等大的%20region,%20各%20region%20分代角色并不固定,按需在%20Eden,%20Survivor,%20Old%20间切换 | 固定大小、固定数量的分代区域 |
| 回收目标 | 回收价值高的%20region%20动态组成的回收集合 | 新生代、整个堆内存 |
跨代引用:各%20region%20除了用卡表标记各卡页是否为%20dirty%20之外,还用哈希表记录了各卡页正在被哪些%20region%20引用,通过这种“双向指针”机制,能直接找到%20Old%20区,避免了全量扫描(G1%20自身内存开销大头)G1%20GC%20有%203%20个阶段(参考其%20GC%20日志)
- 新生代%20GC:Eden%20区占比超阈值触发;标记存活对象并复制到%20Survivor%20区,其内可能有对象会晋升到%20Old%20区
- 老年代%20GC:Old%20区占比达到阈值后触发,执行标记整理
- 初始标记:枚举%20GC%20Roots,已在新生代%20GC%20时顺带完成
- 并发标记:并发执行可达性分析,使用%20SATB%20记录引用变更
- 重新标记:SATB%20分析,避免误回收
- 筛选回收:将%20region%20按回收价值和时间成本筛选组成回收集,STW%20将存活对象拷贝到空%20regions%20后清理旧%20regions,完成回收
- 混合%20GC
参数控制(文档:HotSpot%20GC%20Tuning%20Guide)
| 参数及默认值 | 描述 |
|---|---|
| ‐XX:+UseG1GC | JDK9%20之前手动启用%20G1 |
| -XX:MaxGCPauseMillis=200 | 预期的最大%20GC%20停顿时间;不宜过小,避免每次回收内存少而导致频繁%20GC |
| -XX:ParallelGCThreads=N | STW%20并行线程数,若可用核数%20M%20<%208%20则%20N=1,否则默认%20N=M*5/8 |
| -XX:ConcGCThreads=N | 并发阶段并发线程数,默认是%20ParallelGCThreads%20的%201/4 |
| -XX:InitiatingHeapOccupancyPercent=45 | 老年代%20region%20占比超过%2045%%20则触发老年代%20GC |
| -XX:G1HeapRegionSize=N | 单个%20region%20大小,1~32MB |
| -XX:G1NewSizePercent=5,%20-XX:G1MaxNewSizePercent=60 | 新生代%20region%20最小占整堆的%205%,最大%2060%,超出则触发新生代%20GC |
| -XX:G1HeapWastePercent=5 | 允许浪费的堆内存占比,可回收内存低于%205%%20则不进行混合回收 |
| -XX:G1MixedGCLiveThresholdPercent=85 | 老年代存活对象占比超%2085%,回收价值低,暂不回收 |
| -XX:G1MixedGCCountTarget=8 | 单次收集中混合回收次数 |
2.8、内存分配策略
使用%20Serial%20收集器%20-XX:+UseG1GC%20演示
1.%20对象优先分配在%20Eden%20区
新对象在%20Eden%20区分配,空间不足则触发%20Minor%20GC,存活对象拷贝到%20To%20Survivor,若还是内存不足则通过分配担保机制转移到老年区,依旧不足才%20OOM
byte[]%20buf1%20=%20new%20byte[6%20*%20MB];byte[]%20buf2%20=%20new%20byte[6%20*%20MB];%20//%2010MB%20的%20eden%20区剩余%204MB,空间不足,触发%20minor%20GC
//%20java%20-verbose:gc%20-Xms20m%20-Xmx20m%20-Xmn10m%20-XX:+PrintGCDetails%20-XX:+UseSerialGC%20com.ch03.Allocation//%20minor%20gc%20后新生代内存从%206M%20降到%200.2M,存活对象移到了老年区,总的堆内存用量依旧是%206MB[GC%20(Allocation%20Failure)%20[DefNew:%206823K->286K(9216K),%200.002%20secs]%206823K->6430K(19456K),%200.002%20secs]%20[Times:%20user=0.00%20sys=0.00,%20real=0.00%20secs]%20Heap%20def%20new%20generation%20%20%20total%209216K,%20used%206513K%20eden%20space%208192K,%20%2076%%20used%20//%20buf2from%20space%201024K,%20%2028%%20usedto%20%20%20space%201024K,%20%20%200%%20used%20%20tenured%20generation%20%20%20total%2010240K,%20used%206144K%20the%20space%2010240K,%20%2060%%20used%20//%20buf1
2.%20大对象直接进入老年区
对于%20Serial,%20ParNew,可配置超过阈值%20-XX:PretenureSizeThreshold%20的大对象(连续内存),直接在老年代中分配,避免触发%20minor%20gc,导致%20Eden%20和%20Survivor%20产生大量的内存复制操作
byte[]%20buf1%20=%20new%20byte[4%20*%20MB];
//%20java%20-verbose:gc%20-Xms20m%20-Xmx20m%20-Xmn10m%20-XX:+PrintGCDetails%20-XX:+UseSerialGC//%20-XX:PretenureSizeThreshold=3145728%20com.ch03.Allocation%20//%203145728%20即%203MBHeap%20def%20new%20generation%20%20%20total%209216K,%20used%20843K%20eden%20space%208192K,%20%2010%%20used%20from%20space%201024K,%20%20%200%%20used%20to%20%20%20space%201024K,%20%20%200%%20used%20%20tenured%20generation%20%20%20total%2010240K,%20used%204096K%20%20the%20space%2010240K,%20%2040%%20used%20//%20buf1
3.%20长期存活的对象进入老年代
对象头中%204bit%20的%20age%20字段存储了对象当前%20GC%20分代年龄,当超过阈值-XX:MaxTenuringThreshold(默认%2015,也即%20age%20字段最大值)后,将晋升到老年代,可搭配-XX:+PrintTenuringDistribution观察分代分布
byte[]%20buf1%20=%20new%20byte[MB%20/%2016];byte[]%20buf2%20=%20new%20byte[4%20*%20MB];byte[]%20buf3%20=%20new%20byte[4%20*%20MB];%20//%20触发%20minor%20gcbuf3%20=%20null;buf3%20=%20new%20byte[4%20*%20MB];
//%20java%20-verbose:gc%20-Xms20m%20-Xmx20m%20-Xmn10m%20-XX:+PrintGCDetails%20-XX:+UseSerialGC%20//%20-XX:MaxTenuringThreshold=1%20-XX:+PrintTenuringDistribution%20com.ch03.Allocation[GC%20(Allocation%20Failure)%20[DefNewDesired%20survivor%20size%20524288%20bytes,%20new%20threshold%201%20(max%201)-%20age%20%20%201:%20%20%20%20%20359280%20bytes,%20%20%20%20%20359280%20total:%204839K->350K(9216K)]%204839K->4446K(19456K),%200.0017247%20secs]%20//%20至此,buf1%20熬过了第一次收集,age=1[GC%20(Allocation%20Failure)%20[DefNewDesired%20survivor%20size%20524288%20bytes,%20new%20threshold%201%20(max%201):%204446K->0K(9216K)]%208542K->4438K(19456K)]%20Heap%20def%20new%20generation%20%20%20total%209216K,%20used%204178K%20eden%20space%208192K,%20%2051%%20used%20from%20space%201024K,%20%20%200%%20used%20//%20buf1%20在第二轮收集中被提前晋升to%20%20%20space%201024K,%20%20%200%%20used%20%20tenured%20generation%20%20%20total%2010240K,%20used%204438K%20%20the%20space%2010240K,%20%2043%%20used
4.%20分代年龄动态判定
-XX:MaxTenuringThreshold并非晋升的最低硬性门槛,当%20Survivor%20中同龄对象超%2050%%20后,大于等于该年龄的对象会被自动晋升,哪怕还没到阈值
5.%20空间分配担保
老年代作为%20To%20Survivor%20区的担保区域,当%20Eden%20+%20From%20Survivor%20中存活对象的总大小超出%20To%20Survivor%20时,将尝试存入老年代。JDK6%20之后,只要老年代的连续空间大于新生代对象的总大小,或之前晋升的平均大小,则只会进行%20Minor%20GC,否则进行%20Full%20GC
3、类文件结构
Class%20文件实现语言无关性,JVM%20实现平台无关性,参考《Java%20虚拟机规范》一个%20Class%20文件描述了一个类或接口的明确定义,文件内容是一组以%208%20字节为单位的二进制流,各数据项间没有分隔符,超过%208%20字节的数据项按%20Big-Endian%20切分后存储。数据项分两种:
- 无符号数:描述基本类型;用%20
u1,u2,u4,u8%20分别表示%201,2,4,8%20字节长度的无符号数;存储数字值、索引序号、UTF-8%20编码值等 表:由无符号数、其他表嵌套构成的复合类型;约定%20
_info%20后缀;存储字段类型、方法签名等3.1、结构定义
3.2、语法
参考文档:The%20class%20File%20Format
ClassFile%20{%20%20u4%20%20%20%20%20%20%20%20%20%20%20%20%20magic;%20%20%20%20%20%20%20%20%20//%20魔数%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20minor_version;%20//%20版本号%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20major_version;%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20constant_pool_count;%20//%20常量池%20%20cp_info%20%20%20%20%20%20%20%20constant_pool[constant_pool_count-1];%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20access_flags;%20%20%20%20%20//%20类访问标记%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20this_class;%20%20%20%20%20%20%20//%20本类全限定名%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20super_class;%20%20%20%20%20%20//%20单一父类%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20interfaces_count;%20//%20多个接口%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20interfaces[interfaces_count];%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20fields_count;%20%20//%20字段表%20%20field_info%20%20%20%20%20fields[fields_count];%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20methods_count;%20//%20方法表%20%20method_info%20%20%20%20methods[methods_count];%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20attributes_count;%20//%20类属性%20%20attribute_info%20attributes[attributes_count];}
magic:魔数,简单识别%20*.class%20文件,值固定为%200xCAFEBABEminor_version,%20major_version:Class%20文件的次、主版本号constant_pool_count:常量池大小+1constant_pool:常量池,索引从%201%20开始,0%20值被预留表示不引用任何常量池中的任何常量;常量分两类- 字面量:如%20UTF8%20字符串、int、float、long、double%20等数字常量
- 符号引用:类、接口的全限定名、字段名与描述符、方法类型与描述符等%20现有常量共计%2017%20种,常量间除了都使用u1%20tag前缀标识常量类型外,结构互不相同,常见的有:
CONSTANT_Utf8_info:保存由%20UTF8%20编码的字符串CONSTANT_Utf8_info%20{%20%20u1%20tag;%20%20%20%20%20%20%20%20%20%20%20//%20值为%201%20%20u2%20length;%20%20%20%20%20%20%20%20//%20bytes%20数组长度,u2%20最大值%2065535,即单个字符串字面量不超过%2064KB%20%20u1%20bytes[length];%20//%20长度不定的字节数组}
CONSTANT_Class_info:表示类或接口的符号引用CONSTANT_Class_info%20{u1%20tag;%20%20%20%20%20%20%20%20//%20值为%207u2%20name_index;%20//%20指向全限定类名的%20Utf8_info%20//%20常量间存在层级组合关系}
CONSTANT_Fieldref_info,%20CONSTANT_Methodref_info,%20CONSTANT_NameAndType_info:成员变量、成员方法及其类型描述符CONSTANT_Fieldref_info%20{%20%20u1%20tag;%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20值为%209%20%20u2%20class_index;%20%20%20%20%20%20%20%20%20//%20所属类%20%20u2%20name_and_type_index;%20//%20字段的名称、类型描述符}CONSTANT_Methodref_info%20{%20%20u1%20tag;%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20值为%2010%20%20u2%20class_index;%20%20%20%20%20%20%20%20%20//%20所属类%20%20u2%20name_and_type_index;%20//%20方法的名称、签名描述符}CONSTANT_NameAndType_info%20{%20%20u1%20tag;%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20值为%2012%20%20u2%20name_index;%20%20%20%20%20%20%20//%20字段或方法的名称%20%20u2%20descriptor_index;%20//%20类型描述符}
如上只列举了其中%205%20种常量的结构,可见常量间通过组合的方式,来描述层级关系
access_flags:类的访问标记,有%2016bit,每个标记对应一个位,比如ACC_PUBLIC对应0x0001,表示类被%20public%20修饰;其他%208%20个标记参考%20Opcodes.ACC_XXXthis_class,%20super_class:指向本类、唯一父类的%20Class_info%20符号常量interface_count,%20interfaces:描述此类实现的多个接口信息fields_count,%20fields:字段表;描述类字段、成员变量的个数及详细信息field_info%20{%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20access_flags;%20%20%20%20%20//%20作用域、static,final,volatile%20等访问标记%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20name_index;%20%20%20%20%20%20%20//%20字段名%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20descriptor_index;%20//%20类型描述符%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20attributes_count;%20//%20字段的属性表%20%20attribute_info%20attributes[attributes_count];}
类型描述符简化描述了字段的数据类型、方法的参数列表及返回值,与%20Java%20中的类型对于关系如下:
基本类型:
Z:boolean,%20B:byte,%20C:char,%20S:short,%20I:int,%20F:float,%20D:double,%20J:long- void%20及引用类型:
V:void - 引用类型:
L:_,类名中的%20.%20替换为%20/,添加%20;%20分隔符,如%20Object%20类描述为Ljava/lang/Object; - 数组类型:每一维用一个前置%20[%20表示%20示例:
boolean%20regionMatch(int,%20String,%20int,%20int)对应描述符为%20(ILjava/lang/String;II)Z methods_count,%20methods:方法表;完整描述各成员方法的修饰符、参数列表、返回值等签名信息method_info%20{%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20access_flags;%20%20%20%20%20//%20访问标记%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20name_index;%20%20%20%20%20%20%20//%20方法名%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20descriptor_index;%20//%20方法描述符%20%20u2%20%20%20%20%20%20%20%20%20%20%20%20%20attributes_count;%20//%20方法属性表%20%20attribute_info%20attributes[attributes_count];}
字段表、方法表都可以带多个属性表,如常量字段表、方法字节码指令表、方法异常表等。属性模板:
attribute_info%20{%20%20u2%20attribute_name_index;%20%20%20//%20属性名%20%20u4%20attribute_length;%20%20%20%20%20%20%20//%20属性数据长度%20%20u1%20info[attribute_length];%20//%20其他字段,各属性的结构不同}
属性有%2020+%20种,此处只记录常见的三种
Code%20属性:存储方法编译后的字节码指令
Code_attribute%20{%20%20u2%20attribute_name_index;%20//%20属性名,指向的%20Utf8_info%20值固定为%20"Code"%20%20u4%20attribute_length;%20%20%20%20%20//%20剩下字节长度%20%20u2%20max_stack;%20%20//%20操作数栈最大深度,对于此方法的栈帧中操作数栈的深度%20%20u2%20max_locals;%20//%20以%20slot%20变量槽为单位的局部变量表大小,存储隐藏参数%20this,实参列表,catch%20参数,局部变量等%20%20u4%20code_length;%20%20%20%20%20%20%20//%20字节码指令总长度%20%20u1%20code[code_length];%20//%20JVM%20指令集大小%20200+,单个指令的编号用%20u1%20描述%20%20u2%20exception_table_length;%20//%20异常表,描述方法内各指令区间产生的异常及其%20handler%20地址%20%20{%20%20%20u2%20start_pc;%20%20%20//%20catch_type%20类型的异常,会在%20[start_pc,%20end_pc)%20指令范围内抛出%20%20%20u2%20end_pc;%20%20%20%20%20%20u2%20handler_pc;%20//%20若抛出此异常,则%20goto%20到%20handler_pc%20处执行%20%20%20u2%20catch_type;%20%20}%20exception_table[exception_table_length];%20%20u2%20attributes_count;%20//%20Code%20属性自己的属性%20%20attribute_info%20attributes[attributes_count];}
LineNumberTable%20属性:记录%20Java%20源码行号与字节码行号的对应关系,用于抛异常时显示堆栈对应的行号等信息。可作为%20Code%20属性的子属性LineNumberTable_attribute%20{%20%20u2%20attribute_name_index;%20u4%20attribute_length;%20%20u2%20line_number_table_length;%20%20{%20%20%20u2%20start_pc;%20%20%20%20%20//%20字节码指令区间开始位置%20%20%20u2%20line_number;%20%20//%20对应的源码行号%20%20}%20line_number_table[line_number_table_length];}
LocalVariableTable%20属性:记录%20Java%20方法中局部变量的变量名,与栈帧局部变量表中的变量的对应关系,用于保留各方法有意义的变量名称LocalVariableTable_attribute%20{%20%20u2%20attribute_name_index;%20u4%20attribute_length;%20%20u2%20local_variable_table_length;%20%20{%20%20%20u2%20start_pc;%20//%20局部变量生命周期开始的字节码偏移量%20%20%20u2%20length;%20%20%20//%20向后生命周期覆盖的字节码长度%20%20%20u2%20name_index;%20%20%20%20%20%20%20//%20变量名%20%20%20u2%20descriptor_index;%20//%20类型描述符%20%20%20u2%20index;%20//%20对应的局部变量表中的%20slot%20索引%20%20}%20local_variable_table[local_variable_table_length];}
其他属性直接参考%20JVM%20文档
3.3、示例
源码:com/cls/Structure.java
package%20com.cls;public%20class%20Structure%20{%20%20%20%20public%20static%20void%20main(String[]%20args)%20{%20%20%20%20%20%20%20%20System.out.println("hello%20world");%20%20%20%20}}
javac%20-g:lines%20com/cls/Structure.java%20编译后,参考%20javap%20反编译得到的正确结果,od%20-x%20--endian=big%20Structure.class%20得出%20class%20文件内容的十六进制表示,解读如下:
cafe%20babe%20#%201.%20%20u4%20魔数,标识%20class%20文件类型0000%200034%20#%202.%20%20u2,u2%20版本号,52%20JDK8%20#%203.%20常量池---1---001f%20#%20u2%20constant_pool_count,31%20项(从%201%20开始计数,0%20预留)0a%20%20%20%20%20%20#%20u1%20tag,10,Methoddef_info,成员方法结构%200006%20%20%20%20#%20u2%20index,6,所属类的%20Class_info%20在常量池中的编号%20%20%20##%20java/lang/Object0011%20%20%20%20#%20u2%20index,17,此方法%20NameAndType%20编号%20%20%20%20%20%20%20%20%20%20%20%20%20##%20<init>:()V---2---09%20%20%20%20%20%20#%209,Fileddef_info,成员变量结构0012%20%20%20%20#%20u2%20index,18,所属类%20Class_info%20编号%20%20%20%20%20##%20java/lang/System0013%20%20%20%20#%20u2%20index,19,此字段%20NameAndType%20编号%20%20%20%20##%20out:Ljava/io/PrintStream---3---08%20%20%20%20%20%20#%208,String_info,字符串0014%20%20%20%20#%20u2%20index,20,字面量编号%20%20%20%20%20##%20hello%20world---4---0a%200015%20%20%20%20#%2021%20%20%20%20##%20java/io/PrintStream0016%20%20%20%20#%2022%20%20%20%20##%20println:(Ljava/lang/String;)V---5---07%20%20%20%20%20%20#%20Class_info,全限定类名0017%20%20%20%20#%20u2%20index,23,字面量编号%20%20%20%20%20##%20com/cls/Structure---6---07%20%20%20%20%20%20#%207,Class_info,类引用0018%20%20%20%20#%2024%20%20%20%20##%20java/lang/Object---7---01%20%20%20%20%20%20#%20Utf8_info,UTF8%20编码的字符串0006%20%20%20%20#%20u2%20length,6,字符串长度3c%2069%206e%2069%2074%203e%20#%20字面量值%20%20%20%20##%20"<init>"---8-16---01%200003%20282956%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"()V"01%200004%20436f6465%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"Code"01%20000f%204c696e654e756d6265725461626c65%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"LineNumberTable"01%200004%206d61696e%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"main"01%200016%20285b4c6a6176612f6c616e672f537472696e673b2956%20%20%20%20##%20"([Ljava/lang/String;)V"01%200010%204d6574686f64506172616d6574657273%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"MethodParameters"01%200004%2061726773%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"args"01%20000a%20536f7572636546696c65%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"SourceFile"01%20000e%205374727563747572652e6a617661%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"Structure.java"---17---0c%20%20%20%20%20%20#%2012,NameAndType,名字及类型描述符0007%20%20%20%20#%20u2%20index,7,字段或方法名字面量编号%20%20%20%20##%20<init>0008%20%20%20%20#%20u2%20index,8,字段或方法结构编号%20%20%20%20%20%20%20##%20()V---18---07%200019%20#%2025%20%20%20%20##%20java/lang/System---19---0c001a%20001b%20%20%20#%2026:27%20%20%20%20##%20out:Ljava/io/PrintStream;---20---01%20000b%2068656c6c6f20776f726c64%20%20%20%20##%20"hello%20world"---21--07%20001c%20#%2028%20%20%20%20##%20java/io/PrintStream---22--0c001d%20001e%20%20%20#%2029:30%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20println:(Ljava/lang/String;)V---23-31---01%200011%20636f6d2f636c732f537472756374757265%20%20%20%20%20%20%20%20%20%20##%20"com/cls/Structure"01%200010%206a6176612f6c616e672f4f626a656374%20%20%20%20%20%20%20%20%20%20%20%20##%20"java/lang/Object%20"01%200010%206a6176%20612f%206c61%206e67%202f53%207973%207465%206d%20%20%20%20%20##%20"java/lang/System"01%200003%206f7574%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"out"01%200015%204c6a6176612f696f2f5072696e7453747265616d3b%20%20##%20"Ljava/io/PrintStream;"01%200013%206a6176612f696f2f5072696e7453747265616d%20%20%20%20%20%20##%20"java/io/PrintStream"01%200007%207072696e746c6e%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20"println"01%200015%20284c6a6176612f6c616e672f537472696e673b2956%20%20##%20"(Ljava/lang/String;)V"0021%20#%204.%20u2,access_flags%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20ACC_PUBLIC%20|%20ACC_SUPER0005%20#%205.%20u2,%20this_class,5%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20--5.Class_info-->%20com/cls/Structure0006%20#%206.%20u2,%20super_class,%206%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20--6.Class_info-->%20java/lang/Object%200000%20#%207.%20u2,%20interface_count,%2000000%20#%208.%20u2,%20fields_count,%2000002%20#%209.%20methods%20count,%202%20%20#%20方法一0001%20%20%20%20#%20u2,%20access_flags,%20ACC_PUBLIC%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%200007%20%20%20%20#%20u2,%20name_index,%207%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20<init>%200008%20%20%20%20#%20u2,%20descriptor_index,%208%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20()V%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%200001%20%20%20%20#%20u2,%20attribute_count,%201%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%200009%20%20%20%20%20%20%20%20#%20u2,%20attribute_name_index,%209%20%20%20%20%20%20%20%20%20%20%20##%20Code%20属性0000%20001d%20%20%20#%20u4,%20attribute_length,%2030%200001%20%20%20%20%20%20%20%20#%20u2,%20max_stack,%201%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%200001%20%20%20%20%20%20%20%20#%20u2,%20max_locals,%201%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%200000%200005%20%20#%20u4,%20code_array_length,%205%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%202a%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20#%20u1,%20aload_0%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20将第%200%20个%20slot%20中的变量%20this%20入栈%20b7%20%20%200001%20%20%20%20%20%20%20%20#%20u1,%20invokespecial%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20执行从%20Object%20继承的%20<init>b1%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20#%20u1,%20return%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20返回%20void0000%20%20%20%20%20%20%20%20#%20u2,%20exception_table_length,%200%20%20%20%20%20%20%20%20%20%20##%20exception%20table%20为空,无异常0001%20%20%20%20%20%20%20%20#%20u2,%20attributes_count,%201%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20Code%20属性本身的子属性000a%20%20%20%20%20%20%20%20%20%20%20%20#%2010%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20LineNumberTable%20属性0000%200006%20%20%20%20%20%20%20#%2060001%20%20%20%20%20%20%20%20%20%20%20%20#%20u2,%20line_number_table_length,%2010000%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20#%20u2,%20start_pc,%2000003%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20#%20u2,%20line_number,%203%20%20#%20方法二0009%20%20%20%20#%20access_flags%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20ACC_PIBLIC%20|%20ACC_STATIC000b%20%20%20%20#%20name_index,%2011%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20main000c%20%20%20%20#%20descriptor_index,%2012%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20([Ljava/lang/String;)V0002%20%20%20%20#%20attribute_count,%2020009%20%20%20%20%20%20%20%20#%20attribute_name_index,%209%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20Code0000%200025%20%20#%20attribute_length,%20370002%20%20%20%20%20%20%20%20#%20max_stack,%202%200001%20%20%20%20%20%20%20%20#%20max_locals,%2010000%200009%20%20#%20code_array_length,%209b2%20%20%200002%20%20%20%20%20%20%20#%20getstatic,%202%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20Field:%20java/lang/System.out:Ljava/io/PrintStream;%20//%20加载静态对象变量12%20%20%2003%20%20%20%20%20%20%20%20%20#%20ldc,%203%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20String:%20"hello%20world"%20%20//%20将常量参数入栈b6%20%20%200004%20%20%20%20%20%20%20#%20invokevirtual,%204%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20Method:%20java/io/PrintStream.println:(Ljava/lang/String;)V%20//%20执行方法b1%20%20%20%20%20%20%20%20%20%20%20%20%20%20#%20return0000%20%20%20%20%20%20%20%20#%20exception_table_length,%2000001%20%20%20%20%20%20%20%20#%20attributes_count,%201000a%20%20%20%20%20%20%20%20#%2010%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20##%20LineNumberTable0000%20000a%20%20%20#%20100002%20%20%20%20%20%20%20%20%20%20%20%20#%20line_number_table_length,%2020000%200005%20%20%20%20%20%20%20%20%20%20%20#%200%20->%2050008%200006%20%20%20%20%20%20%20%20%20%20%20#%208%20->%206
3.4、字节码指令
JVM%20面向操作数栈(operand%20stack)设计了指令集,每个指令由%201%20字节的操作码(opcode)表示,其后跟随%200%20个或多个操作数(operand),指令集列表参考%20Java%20bytecode%20instruction%20listings
- 大部分与数据类型相关的指令,其操作码符号都会带类型前缀,如%20i%20前缀表示操作%20int,剩余对应关系为%20
b:byte,%20c:char,%20s:short,%20f:float,%20d:double,%20l:long,%20a:reference - 由于指令集大小有限(256个),故%20
boolean,%20byte,%20char,%20short%20会被转为int运算
字节码可大致分为六类:
加载和存储指令:将变量从局部变量表%20slot%20加载到操作数栈的栈顶,反向则是存储
//%20将%20slot%200,1,2,3,N%20加载到栈顶,T%20表示类型简记前缀,可取%20i,l,f,d,aTload_0,%20Tload_1,%20Tload_2,%20Tload_3,%20Tload%20n//%20将栈顶数据写回指定的%20slotTstore_0,%20Tstore_1,%20Tstore_2,%20Tstore_3,%20Tstore%20n//%20将不同范围的常量值加载到栈顶,由于%200~5%20常量过于常用,有单独对应的指令,ldc%20则加载普通常量bipush,%20sipush,%20Tconst_[0,1,2,3,4,5],%20aconst_null,%20ldc
运算指令
Tadd,%20Tsub,%20Tmul,%20Tdiv,%20Trem%20%20%20%20%20//%20算术运算:加减乘除,取余Tneg,%20Tor,%20Tand,%20Txor%20%20%20%20%20%20%20%20%20%20%20%20//%20位运算:取反、或、与、异或dcmpg,%20dcmpl,%20fcmpg,%20fcmpl,%20lcmp%20//%20比较运算:后缀%20g%20即%20greater,%20l%20即%20less%20thaniinc%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20局部自增运算,与%20iload%20搭配使用
强制类型转换指令:窄化转换为%20T%20类型(长度为%20N)时,会直接丢弃除了低%20N%20位外的其他位,可能会导致数据溢出、正负号不确定,浮点数转整型则会丢失精度
i2b%20//%20int%20->%20bytei2c,%20i2s;%20l2i,%20f2i,%20d2i;%20d2l,%20f2l;%20d2f
对象创建与访问指令:类实例、数组都是对象,存储结构不同,创建和访问指令有所区别
new%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20创建类实例newarray,%20annewarray,%20multianewarry%20%20%20%20%20%20//%20创建基本类型数组、引用类型数组、多维引用类型数组getfield,%20putfield;%20getstatic,%20putstatic%20//%20读写类实例字段;读写类静态字段Taload,%20Tastore;%20arraylength%20%20%20%20%20%20%20%20%20%20%20%20%20//%20读写数组元素;计算数组长度instanceof;%20checkcast%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20校验对象是否为类实例;执行强制转换
操作数栈管理指令
pop,%20pop2%20%20%20%20%20%20%20//%20弹出栈顶%201,2%20元素dup,%20dup2;%20swap%20//%20复制栈顶%201,2%20个元素并重新入栈;交换栈顶两个元素
控制转移指令:判断条件成立,则跳转到指定的指令行(修改%20PC%20指向)
if_<icmpeq,icmpne;icmplt,icmple;icmpgt,icmpge;acmpe,acmpne>%20//%20整型比较,引用相等性判断if<eq,lt,le,gt,ge,null,nonnull>%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20搭配其他类型的比较运算指令使用
方法调用与返回指令
invokevirtual%20%20%20//%20根据对象的实际类型进行分派,调用对应的方法(比如继承后方法重写)%20invokespecial%20%20%20//%20调用特殊方法,如%20<cint>()V,%20<init>()V%20等初始化方法、私有方法、父类方法invokestatic%20%20%20%20//%20调用类的静态方法invokeinterface%20//%20调用接口方法(实现接口的类对象,但被声明为接口类型,调用方法)invokedynamic%20%20%20//%20TODOTreturn,%20return%20//%20返回指定类型,返回%20void
异常处理指令:
athrow%20抛出异常,异常处理则由%20exception_table%20描述- 同步指令:
synchronized%20对象锁由%20monitorenter,%20monitorexit%20搭配对象的%20monitor%20锁共同实现
4、类加载
4.1、类加载过程
1.%20加载
原理:委托%20ClassLoader%20读取%20Class%20二进制字节流,载入到方法区内存,并在堆内存中生成对应的java.lang.Class对象相互引用
2.%20验证
校验字节流确保符合%20Class%20文件格式,执行语义分析确保符合%20Java%20语法,校验字节码指令合法性
3.%20准备
在堆中分配类变量(static)内存并初始化为零值,主义还没到执行%20putstatic%20指令赋值的初始化阶段,但静态常量属性除外:
public%20class%20ClassX%20{final%20static%20int%20n%20=%202;%20%20%20%20%20%20%20%20%20%20//%20常量的值在编译期就已知,准备阶段完成赋值,值存储在%20ConstantValuefinal%20static%20String%20str%20=%20"str";%20//%20字符串静态常量同理}static%20final%20java.lang.String%20str;descriptor:%20Ljava/lang/String;flags:%20ACC_STATIC,%20ACC_FINALConstantValue:%20String%20str
4.%20解析
将常量池中的符号引用(Class_info,%20Fieldref_info,%20Methodref_info)替换为直接引用(内存地址)
5.%20初始化
javac%20会从上到下合并类中%20static%20变量赋值、static%20语句块,生成类构造器()V,在初始化阶段执行,此方法的执行由%20JVM%20保证线程安全;注意%20JVM%20规定有且仅有的,会立即触发对类初始化的六种%20case
public%20class%20ClassX%20{%20%20%20%20static%20{%20%20%20%20%20%20%20%20println("main%20class%20ClassX%20init");%20//%201.%20main()%20所在的主类,总是先被初始化%20%20%20%20}%20%20%20%20public%20static%20void%20main(String[]%20args)%20throws%20Exception%20{%20%20%20%20%20%20%20%20//%20首次会触发类的初始化%20%20%20%20%20%20%20%20//%20SubX%20b%20=%20new%20SubX();%20%20//%20new%20对象%20//%202.%20new,%20getsatic,%20putstatic,%20invokestatic%20指令%20%20%20%20%20%20%20%20//%20println(SuperX.a);%20%20%20%20//%20读写类的%20static%20变量,或调用%20static%20方法%20%20%20%20%20%20%20%20%20//%20println(SubX.c);%20%20%20%20%20%20//%203.%20子类初始化,会触发父类初始化%20%20%20%20%20%20%20%20//%20println(SubX.a);%20%20%20%20%20%20//%20%20%20%20子类访问父类的静态变量,只会触发父类初始化%20%20%20%20%20%20%20%20//%20不会触发类的初始化%20%20%20%20%20%20%20%20//%20println(SubX.b);%20%20%20%20%20%20//%201.%20访问类的静态常量(基本类型、字符串字面量)%20%20%20%20%20%20%20%20//%20println(SubX.class);%20%20//%202.%20访问类对象%20%20%20%20%20%20%20%20//%20println(new%20SubX[2]);%20//%203.%20创建类的数组%20%20%20%20}}class%20SuperX%20{%20%20%20%20static%20int%20a%20=%200;%20%20%20%20static%20{%20%20%20%20%20%20%20%20println("class%20SuperX%20initiated");%20%20%20%20}}class%20SubX%20extends%20SuperX%20{%20%20%20%20final%20static%20double%20b%20=%200.1;%20%20%20%20static%20boolean%20c%20=%20false;%20%20%20%20static%20{%20%20%20%20%20%20%20%20println("class%20SubX%20initiated");%20%20%20%20}}
4.2、类加载器
层级关系双亲委派机制
- 原理:一个类加载器收到加载某个类的请求时,会先委派上层的父类加载器去加载,逐层向上,当父类加载器逐层向下反馈都无法加载此类后,该类加载器才会尝试自己加载;此模型保证了,诸如%20rt.jar%20中的java.lang.Object类,不论在底层哪种类加载器中都一定是被%20Bootstrap%20类加载器加载,%20JVM%20中仅此一份,保证了一致性
实现
//%20java/lang/ClassLoaderprotected%20Class<?>%20loadClass(String%20name,%20boolean%20resolve)%20throws%20ClassNotFoundException%20{%20%20synchronized%20(getClassLoadingLock(name))%20{%20%20%20%20%20%20//%201.%20先检查自己的加载器是否已加载此类%20%20%20%20%20%20Class<?>%20c%20=%20findLoadedClass(name);%20%20%20%20%20%20if%20(c%20==%20null)%20{%20%20%20%20%20%20%20%20%20%20long%20t0%20=%20System.nanoTime();%20%20%20%20%20%20%20%20%20%20try%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(parent%20!=%20null)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%202.%20还有上层则委派给上层去加载%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20c%20=%20parent.loadClass(name,%20false);%20%20%20%20%20%20%20%20%20%20%20%20%20%20}%20else%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%203.%20如果没有上级,则委派给%20Bootstrap%20加载%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20c%20=%20findBootstrapClassOrNull(name);%20%20%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20%20%20}%20catch%20(ClassNotFoundException%20e)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20类不存在%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20%20%20if%20(c%20==%20null)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%204.%20到自己的%20classpath%20中查找类,用户自定义%20ClassLoader%20自定义了查找规则%20%20%20%20%20%20%20%20%20%20%20%20%20%20long%20t1%20=%20System.nanoTime();%20%20%20%20%20%20%20%20%20%20%20%20%20%20c%20=%20findClass(name);%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20}%20%20%20%20%20%20if%20(resolve)%20{%20%20%20%20%20%20%20%20%20%20resolveClass(c);%20%20%20%20%20%20}%20%20%20%20%20%20return%20c;%20%20}}
5、字节码执行引擎
5.1、运行时栈帧结构
public%20static%20void%20main(String[]%20args)%20{%20%20%20%20int%20a%20=%201008611;%20%20%20%20int%20b%20=%20++a;}
对应运行时栈帧结构:
- 局部变量表:大小在编译期确定,用于存放实参和局部变量,以大小为%2032%20bit%20的变量槽为最小单位
- long,%20double%20类型被切分为%202%20个%20slot%20同时读写(单线程操作,无线程安全问题)
- 类对象调用方法时,slot%200%20固定为当前对象的引用,即this隐式实参
变量槽有重用优化,当%20pc%20指令超出某个槽中的变量的作用域时,该槽会被其他变量重用
public%20static%20void%20main(String[]%20args)%20{%20%20{%20%20%20%20%20%20byte[]%20buf%20=%20new%20byte[10%20*%201024%20*%201024];%20%20}%20%20System.gc();%20%20//%20buf%20还在局部变量表的%20slot%200%20中,作为%20GC%20Root%20无法被回收%20%20//%20int%20v%20=%200;%20//%20变量%20v%20重用%20slot%200,gc%20生效%20%20//%20System.gc();
操作数栈:最大深度在编译期确定,与局部变量表配合入栈、执行指令、出栈来执行字节码指令
- 返回地址:遇到return%20族指令则正常调用完成,发生异常但异常表中没有对应的%20handler%20则异常调用完成;正常退出到上层方法后,若有返回值则压入栈,并将程序计数器恢复到方法调用指令的下一条指令
5.2、方法调用
虚方法、非虚方法
非虚方法:编译期可知(程序运行前就唯一确定)、且运行期不可变的方法,在类加载阶段就会将方法的符号引用解析为直接引用。有%205%20种:
- 静态方法(与类唯一关联):invokestatic调用
- 私有方法(外部不可访问)、构造器方法、父类方法:invokespecial调用
- final%20方法(无法被继承):由invokevirtual调用(历史原因)
```java
public%20class%20StaticResolution%20{
%20%20public%20static%20void%20doFunc()%20{
%20%20} %20%20public%20static%20void%20main(String[]%20args)%20{%20%20System.out.println("do%20func...");
%20%20} }%20%20StaticResolution.doFunc();
stack=0,%20locals=1,%20args_size=1%20%20%20%20//%20静态方法的调用版本,在编译时就以常量的形式,存入字节码的参数 %20%20%20%200:%20invokestatic%20%20#5%20%20%20%20%20%20%20%20%20%20%20//%20Method%20doFunc:()V %20%20%20%20%20%20%20%203:%20return
虚方法:需在运行时动态确定直接引用的方法,由`invokevirtual`,%20`invokeinterface`调用<a%20name="nhq1d"></a>####%20静态分派、动态分派背景:方法可被重载(参数类型不同,或数量不同)、可被重写(子类继承后覆盖)<br%20/>分派:对象可声明为类、父类、实现的接口等类型,当对象作为实参或调用方法时,需根据其静态类型或实际类型,才能确定要调用的方法的版本,进而确定其直接引用。此过程即方法的分派<br%20/>reference%20变量的%202%20种类型-%20静态类型:变量被声明的类型,不会改变,编译期可知-%20实际类型:变量指向的对象可被替换,运行时随时可能修改<a%20name="dvFhJ"></a>#####%20静态分派-%20原理:方法重载时,依赖参数的静态类型,来确定要使用哪个重载版本的方法-%20特点:发生在编译阶段,由%20javac%20确定类型“匹配度最高的”重载版本,来作为invokevirtual的参数```javapublic%20class%20StaticDispatch%20{%20%20%20%20static%20abstract%20class%20Human%20{}%20%20%20%20static%20class%20Man%20extends%20Human%20{}%20%20%20%20static%20class%20Woman%20extends%20Human%20{%20}%20%20%20%20public%20void%20f(Human%20human)%20{System.out.println("f(Human)");}%20%20%20%20public%20void%20f(Man%20man)%20{System.out.println("f(Man)");}%20%20%20%20public%20void%20f(Woman%20woman)%20{System.out.println("f(Woman)");}%20%20%20%20public%20static%20void%20main(String[]%20args)%20{%20%20%20%20%20%20%20%20Human%20man%20=%20new%20Man();%20%20%20%20%20//%20静态类型都是%20Human%20%20%20%20%20%20%20%20Human%20woman%20=%20new%20Woman();%20//%20实际类型分别为%20Man,%20Woman%20%20%20%20%20%20%20%20StaticDispatch%20sd%20=%20new%20StaticDispatch();%20%20%20%20%20%20%20%20sd.f(man);%20%20%20//%20f(Human)%20//%20invokevirtual%20#13%20//%20Method%20f:(Lcom/ch08/StaticDispatch$Human;)V%20%20%20%20%20%20%20%20sd.f(woman);%20//%20f(Human)%20//%20编译期就已确定重载版本,写入字节码中%20%20%20%20}}
动态分派
- 原理:方法重写时,依赖%20Receiver%20对象的实际类型,来确定要使用哪个类版本的方法
- 特点:发生在运行时,依赖
invokevirtual指令来确定调用方法的版本,进而实现多态,解析流程为
注:类的方法查找是高频操作,JVM 会在方法区中为类建一张虚方法表 vtable,以实现方法的快速查找
public class DynamicDispatch {static abstract class Human {protected abstract void f();}static class Man extends Human {@Overrideprotected void f() {System.out.println("Man f()");}}static class Woman extends Human {@Overrideprotected void f() {System.out.println("Woman f()");}}public static void main(String[] args) {Human man = new Man(); // 虽然静态类型都是 HumanHuman woman = new Woman();man.f(); // Man f() // invokevirtual #6 // Method com/ch08/DynamicDispatch$Human.f:()Vwoman.f(); // Woman f() // 虽然字节码指令的参数,都是静态类型方法的符号引用man = new Woman();man.f(); // Woman f() // 但 invokevirtual 会根据 Receiver 实际类型,在运行时解析到实际类的直接引用}}
注意,类的字段读写指令getfield, putfield没有invokevirtual的动态分派机制,即子类的同名字段会直接覆盖父类的字段。示例:
public class FieldHasNoPolymorphic {static class Father {public int money = 1;public Father() {money = 2;showMoney();}public void showMoney() { System.out.println("Father, money = " + money); }}static class Son extends Father {public int money = 3; // 子类字段在类加载的准备阶段被赋零值public Son() { // 子类构造器第一行默认隐藏调用 super()money = 4;showMoney();}public void showMoney() { System.out.println("Son, money = " + money); }}public static void main(String[] args) {Father guy = new Son();System.out.println("guy, money = " + guy.money);}}// Son, money = 0 // Father 类构造器执行,动态分派执行了 Son::showMoney()// Son, money = 4 // Son 类构造器中访问最新的、自己的 money 字段// guy, money = 2 // 字段的读写没有动态分派,静态类型是谁,就访问谁的字段
单分派、多分派
方法的 Receiver,与方法的参数,都是方法的宗量,根据一个宗量来选择目标方法称为单分派,需要多个宗量才能确定方法的叫多分派
- 静态分派机制会让编译器在编译阶段,对重载的多个方法,会选出参数匹配度最高的作为目标方法
- 动态分派机制在运行时,依赖 Receiver 实际类型,配合
invokevirtual定位唯一的实例方法作为目标方法
综上两点,Java 是静态多分派、动态单分派的语言
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论