Java虚拟机
关于虚拟机的内容大部分都可以包含在内存管理、垃圾回收、优化工具、代码加载执行这几部分。
1 内存管理
1.1 区域划分
JVM的内存区域由其管理并根据职能划分为不同的数据区域,根据《Java虚拟机规范》的规定,内存区域划分为:
- 程序计数器
- 方法区
- 虚拟机栈
- 本地方法栈
- 堆
其中方法区和堆为线程共享、其他为非共享。
程序计数器 简单来说,「程序计数器」是线程维度的,用于标识线程当前执行的字节码位置的行号指示器。如果线程执行的是Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,那么为空。
多线程情景下的线程切换(切换到上次执行的位置),分支、循环、跳转、异常处理等等都依赖程序计数器。
虚拟机栈 Java虚拟机栈是线程维度的,用于存储线程执行过程中方法粒度的信息,这些信息存储在创建的栈帧中,包含局部变量表、操作栈、方法出口等信息。每一个方法执行的过程,就对应一个入栈到出栈的过程。
这个区域可能出现如下两种异常:
- StackOverFlow: 栈深度的问题。
- OutOfMemory:内存不足的问题。
本地方法栈 按照虚拟机规范来讲,本地方法栈和虚拟机栈的区别在于虚拟机栈负责Java方法的执行,本地方法栈负责Native方法的执行。HotSpot虚拟机无此区分,二者为一个区域。
Java堆 堆区域为线程共享,存放所有类实例的内存区域。堆是垃圾回收管理的主要区域,以此角度分,堆可以分为新生代和老年代。
Java堆被划分为两个区域:新生代(young) + 老年代(old)。前者默认占 1/3堆空间,后者2/3。针对于JDK1.7以前,堆区域还存在Perm区,用于存放类、常量等信息。
同样,这对于GC,分为Minor GC、Full GC两种。
Minor GC也成为Young GC,主要流程:
- 对象创建在Eden 或者 From 区(当对象较大,需要连续的内存空间时,可能直接分配在 Old区)。
- 一次Minor GC,如果对象仍然存活,那么将其移到 To Survivor 区(如果能够放得下),清理 Eden和From区,并将存活的年龄加1.
- 当对象的年龄到达15(通过XX:MaxTenuringThreshold设置)时,这些对象复制到Old区。
通常情况下,Old GC 会同时对Young/Old区进行清理操作。
方法区 方法区为线程共享的,用于存放类、常量、静态变量、编译后的代码等数据。低版本的HotSpot,使用「永久区」的方式实现方法区。
运行时常量池是方法区的一部分,用于存放编译期或动态生成的字符常量和符号引用。
除以上虚拟机运行内存区之外,直接内存(Direct Memory)也可以被JVM通过Native方法使用,这类内存不受JVM分配的内存大小的限制,但是同样会因为内存不足导致 OutOfMemroy。
1.2 一个例子
以 MyObj o = new MyObj()为例:
- 「MyObj o」会以 reference 类型出现在JVM内存区域中的「栈」本地变量表中。
- 「new MyObj()」在堆上进行内存分配。
- 在「方法区」中包含此对象的类型数据,如类型,父类,接口,方法等。
以上,为JVM虚拟机内存区域划分,以及每个内存区域的作用与相互之间的联系。
2 垃圾回收
垃圾回收的讨论无非是四个问题:
- What: 什么对象需要回收?
- When: 什么时候进行回收?
- How: 怎么回收?
- Why: 为什么这样回收?
What
‘已死’的对象需要被回收,判断对象已死通常是通过是否存在与「根对象」之间的引用而不是通过自身引用数来判断,后者通常使用「引用计数算法」实现,问题是无法解决相互引用的问题,而「根搜索」就是用来判断对象与根对象之间的引用关系的算法。
通常意义的引用是指reference类型的值为被引用对象内存的起始地址,在垃圾回收的范畴内讲,引用还可以细化为:strong引用、soft引用、weak引用、phantom引用。强引用为通常意义上Class a = new A()的引用,只要引用存在,就不会被垃圾回收。软引用的对象在OOM之前会触发回收操作,如果对这类对象的回收后仍然内存不足,才会OOM。弱引用的对象只要进行垃圾回收,不论内存是否充足,都会被进行回收。
How
垃圾收集器构建与垃圾收集算法之上,常见的垃圾收集算法有:
- 标记-清除(Mark-Sweep)算法。这种算法的问题是因为不连续内存的会说会产生比较多的内存碎片,当需要连续内存分配时,仍然会出现内存不足,所以,针对于使用这种算法的垃圾收集器通常在此操作之后还会进行一次compact操作以减少内存碎片的问题。
- 复制算法。复制的前提是区域分块,当前的堆分区也是这个思想的体现。默认的新生代区域划分为Eden(8/10)、From Survivor(1/10)、To Survivor(1/10),为什么存在两个S区的原因也是为了复制的实现,两个S区同时只有一个是存在存活对象的,一次回收操作会将存在数据的E区和S区存活对象复制到另一个S区,然后清空掉这两个区域。当目标S区空间不足时,那么直接进老年代。新生代区域8:1:1的划分有两点理论依据,其一是98%的对象的生命周期都很短(IBM),其二是为了节约内存空间,即如果存在内存浪费,最多也就10%,但是这个理论跑通的前提是当S区不足时可以有老年代的区域保证。这也就意味着老年代的垃圾收集与回收并不可以采用这种办法,所以有下面的MC算法。
- 标记-合并(Mark-Compact)算法。这种算法解决的就是Mark-Sweep的内存碎片问题。
- 分代回收算法。年轻代老年代这个东西。
垃圾回收器 讨论垃圾回收器一般就是两个点:
- 底层的垃圾回收算法
- 工程情景与垃圾回收器的选择,主要是GC时间和吞吐量的考虑。
年轻代 | 老年代 |
---|---|
Serial | CMS |
ParNew | Serial Old |
Parallel Scavenge | Parallel Old |
G1 | G1 |
上图中同一行并不代表固定的组合关系。
- Serial: 串行回收。
- ParNew: 多线程回收,较Serial尽量减小了 「Stop The World」的停顿时间。
- Parallel Scavenge:关注点在于提高吞吐量而不是停顿时间。
- CMS:老年代回收器,关注点在于减小停顿时间,使用Mard-Sweep算法。
- G1:通过「切分」的思想提升GC效果。