1. 运行时数据区域
程序计数器:线程私有,当前线程执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能依赖这个计数器。如果执行Native方法,计数器值为空。此区域是唯一不存在OutOfMemoryError的区域
Java虚拟机栈:线程私有,每一个java方法都有一个栈帧,方法从调用到执行完成,对应着一个栈帧在虚拟机栈中入栈到出栈。虚拟机栈中的局部变量表在编译期间完成内存空间分配,不改变。会有Stack OverflowError和OutOfMemoryError。
本地方法栈:与Java虚拟机栈相似,但他为Native方法服务。
Java堆:内存中最大的一块,线程共享。只存放对象实例。垃圾收集器管理的主要区域,也成为GC堆。分为新生代和老年代(Eden空间,from survivor空间,to survivor空间)。可以物理空间不连续。OutOfMemoryError
方法区:线程共享,存储已被虚拟机加载的类信息、常量、静态变量和代码等。OutOfMemoryError
运行时常量池:是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。在运行期间也可能产生新的常量。
直接内存:java堆中的directbyteBuffer对象是直接内存的引用,他是native函数库直接分配的对外内存。可以调高性能,避免Java堆和native堆的来回复制数据。
2. HotSpot对象
2.1 对象创建
- 检查指令参数是否能常量池中定位到类符号引用。如果没有,执行类加载
- 虚拟机为新生对象分配内存。频繁创建对象过程中可能存在线程安全问题。
- 将内存空间初始化为0
- 设置对象头
- 执行代码的<init>方法
2.2 对象的内存布局
对象分为三块区域:对象头、实例数据、对齐填充
对象头包括:Mark Word(32bit或64bit)+类型指针(指向他的类元数据的指针)
实例数据:对象真正存储的有效信息
对齐填充:HotspotVM要求对象起始地址必须是8字节的整数倍。对象大小必须是8字节的整数倍。
2.3 对象的访问定位
Java程序通过栈上的reference数据操作堆上的具体对象
访问方式:句柄、直接指针
句柄:Java堆中有一部分内存作为句柄池。Reference中存储的是对象的句柄地址。句柄包含了对象示例数据与类型数据的具体地址。对象地址改变时只需要改变句柄池
直接指针:reference存储对象地址。速度更快,节省了一次指针定位开销
3. 垃圾回收机制概述
Garbage collection ,GC需要考虑的3件事情:
哪些内存需要回收? java堆和方法区
什么时候回收?
如何回收?
3.1 判断对象是否死亡
引用计数算法:存在一个引用就+1,如果计数器为0则不能在被使用。但很难解决对象之间相互循环引用的问题。
可达性分析算法:从GC roots节点开始搜索,走过的路径称为引用链。当一个对象到GC roots没有引用链时,不可用。可以作为GC roots的对象是:虚拟机栈(栈帧中的本地变量表)中引用的对象,方法区找那个类静态属性引用的对象,方法去中敞亮引用的对象,本地方法栈(Native方法)引用的对象。
对象是否存活与“引用”有关。
引用分为强引用、软引用、弱引用、虚引用。
方法区,永久代的垃圾手机主要回收两部分:废弃常量和无用的类。
判断“无用的类”:该类所有实例都被回收了,加载该类的classloader已经被回收了,对象没有在任何地方被引用。
3.2 垃圾收集算法
标记-清除算法:首先标记出所有需要回收的对象,标记完成后统一回收。
不足:效率不高;空间碎片太多
复制算法:将内存容量划分为大小相等的两块,一块内存用完后,将还存活的对象复制到另一块上。清除已经使用过的内存。
不足:内存缩小了一半,代价较高。但IBM证明不需要1:1的空间划分,只要将一大块给Eden空间,两个较小块给survivor空间。每次使用Eden和其中一个survivor。
Hotspot默认Eden和survivor比例为8:1。只有10%会被“浪费”。当survivor空间不够用是需要依赖其他内存进行分配担保。
标记-整理算法:复制手机算法在对象存活率高时,效率会变低。在老年代不能选用这种算法。标记需要回收的对象,然后让所有存活的对象向一端移动。然后清理掉待回收的对象。
分代收集法:商业虚拟机采用分代收集。将Java堆分为新生代和老年代,根据各个年代特点采用合适的收集算法。新生代——复制算法,老年代——标记清理或标记整理算法。
4. Hotspot的算法实现
可达性分析必须在整个执行系统看起来像是被冻结在一个时间点上。即GC进行时,停顿所有Java线程。Hotspot使用OopMap数据结构记录哪些地方放着对象引用。GC扫描时可以直接得知这些信息。
安全点是程序执行时可以安全停止的点。OopMap也不是每条指令一个。具有“指令序列复用”,例如调用、循环跳转等才会产生safepoint。通过抢先式中断和主动式中断使得在安全点上所有线程都停下来。
安全区域是在程序不执行或没有CPU时间时,JVM进行GC的位置。是指在一段代码片段中,引用关系不发生变化。
5. 垃圾收集器
5.1 Serial收集器(单线程)
单线程收集器,进行GC时,必须暂停其他所有的工作线程。
新生代采用复制算法,老年代采用标记-整理算法
优点:简单而高效。
5.2 ParNew收集器(多线程)
是serial收集器的多线程版本。使用多条线程进行GC,其他与serial收集器所有参数完全相同。
许多运行在server模式下的虚拟机中首选Parnew收集器。因为目前只有它能与CMS收集器配合工作。
在单CPU环境下parnew不会比serial收集器更好。
5.3 Parallel Scanvenge收集器(吞吐量优先)
新生代收集器,复制算法,目标是达到一个可控制的吞吐量。可以高效地利用CPU时间。
两个参数:控制最大垃圾收集停顿时间和吞吐量大小。GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的,GC会变得频繁。
可以控制GC自适应调节策略
5.4 Serial Old收集器
Serial收集器的老年代版本。使用标记-整理算法。
用途:给client模式下的虚拟机使用;parallel scanvenge收集器搭配(jdk1.5之前);CMS收集器的后备
5.5 Parallel Old收集器
parallel scanvenge收集器的老年代版本。使用多线程和标记-整理算法。
在注重吞吐量以及CPU资源敏感的场合,优先考虑parallel scanvenge和parallel old组合。
5.6 CMS收集器
以获取最短回收停顿时间为目标的收集器。标记-清除算法。
过程分为四步:初始标记(标记GC Root能直接关联的对象,速度快)、并发标记、重新标记(修正并发标记期间用户程序运作导致的标记变动)和并发清除。
缺点:对CPU资源敏感;无法处理浮动垃圾,如果CMS GC失败,启动serial old进行垃圾回收;会产生空间碎片,有参数控制隔一段时间进行FullGC
5.7 G1收集器(garbage-first)
面向服务端应用的垃圾收集器。
特点:并行与并发;分代收集;空间整合;可预测的停顿
将Java堆分成多个大小相等的独立区域,不再是物理隔离
维护一个垃圾堆积的价值大小的优先列表,优先回收价值最大的区域
步骤:初始标记、并发标记、最终标记、筛选回收
5.8 GC日志
GC发生时间(jvm启动开始)、GC时停顿类型、发生的区域、内存区域变化、java堆可使用容量变化
6 内存分配与回收策略
6.1 对象优先在Eden分配
优先分配Eden,如果Eden没有足够的空间,发起一次MinorGC
6.2 大对象直接进入老年代
需要大量连续内存空间的java对象,典型的是很长的字符串或数组,直接进入老年代
6.3 长期存活的对象进入老年代
每个对象定义对象年龄计数器。初始age为0,每熬过一次Minor GC,年龄加一。Age=1时到survivor。Age增加到一定程度(默认15)进入老年代。
6.4 动态对象年龄判定
当survivor空间中相同年龄对象大小大于survivor空间的一半,大于这个age的对象进入老年代。
6.5 空间分配担保
Minor GC前,先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果大于,GC确保安全;如果不大于,检查是否允许担保失败,允许担保失败时检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于可以尝试GC,如果小于改为FULL GC