Java 虚拟机
运行时数据区
-
程序计数器 线程私有,唯一不存在OOM区域
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。 -
Java 虚拟机栈 线程私有,
OutOfMemory
,StackOverFlow
.
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
可以通过-Xss
这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小
java -Xss2M HackTheJava
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出
StackOverflowError
异常; - 栈进行动态扩展时如果无法申请到足够内存,会抛出
OutOfMemoryError
异常。
- 本地方法栈 线程私有,
OutOfMemory
,StackOverFlow
.
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为native
方法服务。 - 堆 线程间共享
OutOfMemory
所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
新生代(Young Generation)
老年代(Old Generation)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过-Xms
和-Xmx
这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
- 方法区 线程间共享
OutOfMemoryError
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
为了更容易管理方法区,从 JDK1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 - 运行时常量池
OutOfMemoryError
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。 - 直接内存
OutOfMemoryError
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
垃圾收集
垃圾收集主要是针对堆和方法区进行。
- 引用计数算法 避免循环引用
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被
回收。 - 可达性分析算法
引用类型
- 强引用
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
- 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
- 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
- 虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
垃圾回收算法
- 标记-清除
标记和清除过程效率都不高
;大量不连续的内存碎片
- 在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
- 在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会
合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍
历这个空闲链表,就可以找到分块。 - 在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返
回这个分块;如果找到的块大于size
,会将块分割成大小为size
与(block - size)
的两部分,返回大小为 size 的分块,并把大小为(block - size)
的块返回给空闲链表。 - 标记-整理
不会产生内存碎片;
需要移动大量对象,处理效率比较低。
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 - 复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 - 分代收集
新生代使用:复制算法
老年代使用:标记 - 清除
或者标记 - 整理
算法
Serial收集器
单线程
串行
在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
ParNew收集器
多线程
串行
它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
Parallel Scavenge收集器
多线程
达到可控的吞吐量
与 ParNew 一样是多线程收集器。
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
Serial Old收集器
Serial 收集器的老年代版本
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。
Parallel Old收集器
Parallel Scavenge 收集器的老年代版本
是 Parallel Scavenge 收集器的老年代版本。
CMS收集器
分为以下四个流程:
- 初始标记:仅仅只是标记一下
GC Roots
能直接关联到的对象,速度很快,需要停顿。 - 并发标记:进行
GC Roots Tracing
的过程,它在整个回收过程中耗时最长,不需要停顿。 - 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
G1收集器
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
内存分配与回收策略
Minor GC 和 Full GC
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC慢很多。
Full GC触发条件
- 调用 System.gc()
- 老年代空间不足
- 空间分配担保失败
- JDK 1.7 及以前的永久代空间不足
- Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时
性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
类加载机制
类加载过程包含了加载、验证、准备、解析和初始化这 5 个阶段。
加载(Loading)
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
验证(Verification)
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备(Preparation)
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是0。
public static final int value = 123;
解析(Resolution)
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
初始化(Initialization)
初始化阶段是虚拟机执行类构造器 <clinit>()
方法的过程。
<clinit>()
是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
public class Test {
static {
i = 0;
// 给变量赋值可以正常编译通过
System.out.print(i);
// 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
类加载器
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果为
true
,也包括使用 instanceof
关键字做对象所属关系判定结果为 true
。
- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在
<JRE_HOME>\lib
目录中的,或者被-Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,
用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 - 扩展类加载器(Extension ClassLoader)这个类加载器是由ExtClassLoader(
sun.misc.Launcher$ExtClassLoader
)实现的。它负责将<JAVA_HOME>/lib/ext
或者被java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader)这个类加载器是由AppClassLoader(
sun.misc.Launcher$AppClassLoader
)实现的。由于这个类加载器是 ClassLoader 中的getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath
)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}