《深入理解Java虚拟机》第六章 类文件结构

6.1 概述

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

6.2 无关性的基石

Java虚拟机有两个无关性,即平台无关性和语言无关性。字节码(ByteCode) 是构成平台无关性的基石。在 Java 发展之初,设计者就曾经考虑过并实现了让其他语言运行在 Java 虚拟机之上的可能性,由此 Java 规范拆分成了 Java语言规范《The Java Language Specification》及 Java 虚拟机规范《The Java Virtual Machine Specification》。

In the future, we will consider bounded extensions to the Java virtual machine to provide better support for the other languages.

  • 语言无关性是指虚拟机并不止执行 Java程序,也考虑让其支持其他语言(Groovy/Scala/Kotlin等)的运行。

  • “一次编写,到处运行”。Java的平台无关性即体现在此处,可以在多个平台上运行。

6.3 Class 类文件的结构

Class 文件是一组以8位字节为基础单位的二进制流。Class 文件采用一种类似于 C 语言结构体的伪结构来存储数据 :

  • 无符号数
    • 基本的数据类型
    • u1 / u2 / u4 / u8 分别代表1个字节 / 2个字节 / 4个字节 / 8个字节
    • 可以用来描述数字 / 索引引用 / 数量值或者按照UTF-8编码构成字符串值
    • 复合数据类型
    • 由多个无符号数或者其他表作为数据项构成,习惯以“_info”结尾
    • 用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表

6.3.1 魔数与 Class 文件的版本

每个 Class 文件的头4个字节称为魔数(Magic Number)。唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。

紧接着魔数的4个字节是 Class 文件的版本号 : 第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

将HelloWorld.java编译成 Class 文件后,使用 Synalyze It 16进制编辑工具查看

也可以使用命令查看 Class 文件的版本号

1
2
3
4
5
6
7
8
9
10
# javap -v HelloWorld.class 
Classfile /Users/lujiahao/HelloWorld.class
Last modified 2019-1-12; size 430 bytes
MD5 checksum f6fad4e65a952d7f01272063b971c2f8
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
...

同时查看本机 JDK 版本 :

1
2
3
4
# java -version  
java version "1.8.0_161"
Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)

通过上述方式我们可以看到 Class 文件的前9个字节的含义。下面是 Class 文件的版本号汇总 :

发布版本号 内部版本号(十六进制) 内部版本号(十进制)
1.5 31 49
1.6 32 50
1.7 33 51
1.8 34 52

Tips

6.3.2 常量池

6.3.3 访问标志

6.3.4 类索引、父类索引与接口索引集合

6.3.5 字段表集合

6.3.6 方法表集合

6.3.7 属性表集合


欢迎大家关注😁

《深入理解Java虚拟机》第二章 Java 内存区域与内存溢出异常

著名数学家华罗庚先生说:“读一本书要越读越薄。”书越读越薄的过程,就是在多次重复阅读中不断删除冗余信息的过程,浓缩的主要办法是:列提纲与写梗概。前者必须在认真读的基础上,理清文章的脉络,然后逐段概括内容;后者也必须反复阅读,掌握课文要点,将内容加以高度浓缩。浓缩法就是博学反约,厚积薄发,把厚书读薄,又把薄书积厚的读书方法。

2.1 概述

从概念上介绍 Java 虚拟机内存的各个区域,讲解这些区域的作用、服务对象以及其中可能产生的问题,这是翻越虚拟机内存管理这堵围墙的第一步。

2.2 运行时数据区域

JVM 内存结构的布局和相应的控制参数 :

2.2.1 程序计数器

  • 一块较小的内存空间,固定宽度的整数的存储空间
  • 线程私有
  • 当前线程所执行的字节码的行号指示器
  • Java 虚拟机规范中唯一一个没有规定 OutOfMemoryError 的区域
  • 如果线程正在执行 Java 方法,存储的是正在执行的虚拟机字节码指令的地址;如果正在执行 Native 方法,其值为空(Undefined)。

经典问题扩展 : Java 程序计数器为什么不规定 OutOfMemoryError ?

2.2.2 Java 虚拟机栈(Java Virtual Machine Stacks)

  • 线程私有,生命周期与线程相同
  • 运行 Java 方法( 字节码 ) 服务
  • 描述的是 Java 方法执行的内存模型 : 栈帧(Stack Frame),包含:局部变量表、操作数栈、动态链接、方法出口等
  • StackOverflowError 异常:如果线程请求的栈深度大于虚拟机所允许的深度,抛出此异常
  • OutOfMemoryError 异常:如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,抛出此异常

2.2.3 本地方法栈(Native Method Stack)

  • 线程私有,生命周期与线程相同
  • 运行 Native方法服务
  • 与Java虚拟机栈相似,也会抛出StackOverflowError异常和OutOfMemoryError异常

2.2.4 Java 堆(Java Heap)

  • JVM 所管理的内存中最大的一块,垃圾回收的主要操作区域
  • 所有线程共享,虚拟机启动时创建
  • 所有对象实例以及数组都要在堆上分配(非绝对,JIT 和逃逸分析技术发展)
  • 物理上不连续的内存空间,逻辑上是连续的
  • 划分为:新生代( Eden空间、From Survivor空间、To Survivor空间 (分配比例 8:1:1) )和老年代
  • 控制参数
    • -Xms 设置堆的最小空间大小
    • -Xmx 设置堆的最大空间大小
    • -XX:NewSize 设置新生代最小空间大小
    • -XX:MaxNewSize 设置新生代最小空间大小。
  • OutOfMemoryError异常:如果堆中没有内存完成实例分配,并且堆也无法扩展,抛出此异常

2.2.5 方法区(Method Area)

  • 所有线程共享
  • Java 虚拟机规范把方法区描述为堆的一个逻辑部分,别名非堆 Non-Heap,包含:类信息、常量、静态变量、即时编译器编译后的代码等数据
  • HotSpot 虚拟机称为“永久代”(Permanent Generation)
  • 回收效率并不高
  • OutOfMemoryError异常:当方法区无法满足内存分配需求时,抛出此异常

2.2.6 运行时常量池(Runtime Constant Pool)

  • 方法区一部分,所有线程共享
  • 存储编译期生成的各种字面量和符号引用
  • OutOfMemoryError 异常:方法区一部分,受到方法区内存限制,当常量池无法再申请到内存时,抛出此异常
  • 扩展 深入解析String#intern

2.2.7 直接内存(Direct Memory)

  • 并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域
  • OutOfMemoryError 异常:配置虚拟机参数时,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现异常

2.3 HotSpot 虚拟机对象探秘

2.3.1 对象的创建

  1. 类加载检查:new 类名,根据 new 的参数在常量池中定位一个类的符号引用,如果没有找到这个符号引用,说明类还未加载,则进行类的加载/解析和初始化。
  2. 虚拟机为对象分配内存(位于堆中)
  3. 将分配的内存初始化为零值(不包括对象头),如果使用 TLAB ,这一过程可以提前至 TLAB 分配时进行
  4. 调用对象的<init>方法

堆内存分配两种方式:指针碰撞(Bump the Pointer) : Java 堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew 等收集器。空闲列表(Free List) : Java 堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS 这种基于 Mark-Sweep 算法的收集器。

堆内存分配并发解决方案:对分配内存空间的动作进行同步处理,实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。本地线程分配缓冲 TLAB (Thread Local Allocation Buffer),把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

2.3.2 对象的内存布局

  • 对象头( Header )
    • 对象自身运行时数据 ( Mark Word ),包含:哈希码 / GC分代年龄 / 锁状态标志 / 线程持有的锁 / 偏向线程ID / 偏向时间戳
    • 类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  • 实例数据( Instance Data )
    • 对象真正存储的有效信息
    • 程序代码中定义的各种类型的字段内容
    • HotSpot 虚拟机默认分配策略:longs/doubles/ints/shorts/chars/bytes/booleans/oops(Oridinary Object Pointers)
  • 对齐填充( Padding ),并不是必然存在的,仅仅起着占位符的作用。

2.3.2 对象的访问定位

  • 使用句柄:Java 堆中分配一块内存,reference 中存储的就是对象句柄地址,使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中实例数据地址,reference 本身不用改变。如下图所示:
  • 直接指针:Java 堆中分配一块内存,reference 中存储的就是对象实例地址,HostSpot 使用此种方式,节省了一次指针定位的时间开销,提升了速度。如下图所示:

2.4 实战:OutOfMemoryError 异常

  • MacBook Pro Retina, 2.6 GHz Intel Core i7, 16 GB 2133 MHz LPDDR3, OS X Yosemite
  • JDK 1.8

2.4.1 Java 堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Java 堆内存溢出
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author lujiahao
* @date 2018-12-21 14:47
*/
public class HeapOOM {
static class OOMObject {

}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
System.out.println(list.size());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1439.hprof ...
Heap dump file created [28440089 bytes in 0.115 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at OutOfMemory.HeapOOM.main(HeapOOM.java:20)

Process finished with exit code 1

2.4.2 虚拟机栈和本地方法栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 虚拟机栈和本地方法栈溢出
* VM Args: -Xss128k
* @author lujiahao
* @date 2018-12-21 17:02
*/
public class JavaVMStackSOF {
private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
1
2
3
4
设置128k,启动会报下面的问题
The stack size specified is too small, Specify at least 160k
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
1
2
3
4
5
修改为161k,实现效果
stack length:7738
Exception in thread "main" java.lang.StackOverflowError
at OutOfMemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:22)
at OutOfMemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:22)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 创建线程导致内存溢出 危险!!! 可能导致死机,我就不轻易尝试了
* VM Args: -Xss2M
* @author lujiahao
* @date 2018-12-21 17:11
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {

}
}

public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}

public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

2.4.3 方法区和运行时常量池溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 运行时常量池导致内存溢出
* VM Args: -XX:PermSize=10m -XX:MaxPermSize=10M
* jdk1.6
*
* 使用新版本的jdk会输出:
* Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0
* Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0
*
* @author lujiahao
* @date 2018-12-21 17:33
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持炸常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<>();
// 10MB的PermSize在Integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 借助CGLib使方法区出现内存溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author lujiahao
* @date 2018-12-21 17:40
*/
public class JavaMethodAreaOOM {
static class OOMObject{}
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(objects, args);
}
});
enhancer.create();
}
}
}

2.4.4 本机直接内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 本机直接内存溢出(使用unsafe分配本机内存)
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author lujiahao
* @date 2018-12-21 17:51
*/
public class DirectMemoryOOM {
public static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

欢迎大家关注😁

《深入理解Java虚拟机》前期准备

为什么要学习 JVM

对于 Java 开发来说 JVM 是一个坎,面试大厂必问内容,是一块硬骨头,同时也是 Java 开发的必备技能。

学习 JVM 的目的:

  • 了解 JVM运行原理,不做”CRUD Boy”
  • 个人技能提升( 呸❗️说实话❗️ 为了装B😏)
  • 面试不怂

工欲善其事必先利其器

  • IDEA
  • Xmind ZEN
  • Google

纸上得来终觉浅,绝知此事要躬行

代码还是要敲的,记录自己的成长历程,你会感谢当时那么努力的自己。

艿艿 在星球里面说要形成自己的“点 -> 线 -> 面”,这其实说的就是知识体系,而思维导图就是生成知识体系的神兵利器。

巨人的肩膀

在线 Java 编译网站

最后在推荐几个在线 Java 编译网站,不想用重重的 IDE 的时候可以试试看😉


欢迎大家关注 : LF工作室

简书 : https://www.jianshu.com/u/e61935d18b09

掘金 : https://juejin.im/user/59239002570c350069c5f0bb

微信公众号 :

头条号 :