目录
对象创建
JVM遇到new指令时,检查符号引用代表的类是否已经被加载,解析和初始化过。没有的话,需要执行类加载的过程。
加载通过后,为新生对象分配内存。对象所需的内存大小,在类加载完成时候便可以完全确定(如何确定参考对象的内存布局)。
为对象分配空间其实就是把一块确定大小的内存从JVM堆中划分出来。这个过程涉及到两个重要的概念:指针碰撞 和 空闲列表。
指针碰撞
假设堆内存是完整的,用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所需要分配的内存就是把这个指针向空闲那一边挪动与对象大小相等的距离,这种分配方式成为指针碰撞
。
如下图所示:
关键词: 堆内存绝对完整。
空闲列表
堆内存不完整,已使用的内存和未使用的内存相互交错,这就没有办法指针碰撞了,虚拟机需要维护一个列表,记录哪些内存块可用,在分配的时候从列表中找到一块足够大得空间划分给对象实例,并更新列表的记录。这种分配方式称为空闲列表。
选择哪种方式由JVM堆是否规整决定,JVM堆是否规整取决于垃圾收集器是否带有压缩整理功能。Serial, ParNew收集器采用的分配方式时指针碰撞,CMS这种基于mark-sweep算法的收集器采用空闲列表的方式。
空间分配并发性问题
JVM对象的创建时非常频繁的行为,指针碰撞方式需要做同步,否则就会导致分配被覆盖,一般有以下两种方式保证同步:
-
CAS+失败重试
-
本地线程分配缓冲(Thread Local Allocation Buffer),简称TLAB
TLAB实际就是每个线程在java堆中预先分配一小块内存,哪个线程需要分配内存,就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
-XX:+/-UseTLAB参数设定虚拟机是否使用TLAB。
分配完内存后,需要对对象进行必要的设置,比如对象时哪个类的实例,对象的哈希码,对象的GC分代年龄等。
对象内存布局
对象在内存中存储的布局分为3部分:对象头,实例数据,对其填充。
对象头(mark word), 包括哈希码、分代年龄、锁标志位、偏向线程ID、偏向时间戳等信息, 在32位虚拟机中占32bit,64位虚拟机中占64bit。
32位JVM的Mark Word默认存储结构如下所示:
垃圾回收算法
这些也是个老生常谈的话题了,记录下备忘。
标记-清除算法
标记清除算法(Mark-Sweep), 分为标记和清除两个阶段。首先标记出所有需要回收的对象,然后回收所有需要回收的对象。
缺点:
- 效率问题:需要扫描所有对象,堆越大,GC越慢
- 产生不连续的内存碎片
复制算法
复制算法(Copying), 将内存划分为两块,每次只使用其中的一块,当一块的内存使用完了,将仅存活的对象复制到另外一块,然后把原来的整个内存一次性清理掉
特点:
- 只需扫描存活的对象,效率更高
- 不会产生碎片
- 需要浪费额外的内存作为复制区
- 适合生命周期比较短的对象,因为每次GC能回收大部分的对象,复制开销比较小
现在的商业虚拟机中使用复制算法回收新生代
标记整理算法
标记整理(Mark-Compact), 标记过程跟之前一样,另所有存活的对象一端移动, 然后直接清理掉这端边界以外的内存。
特点:
- 没有内存碎片
- 比Mark-Sweep耗费更多的时间进行整理
总结一下,这几种手机算法的优劣:
算法 | 核心动作 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
标记清除算法 | 1. 标记需要回收的对象 2. 清除要回收的对象 | – | 1.标记和清除效率低 2.造成内存碎片 | 回收老年代 |
复制算法 | 1.扫描存活对象 2.将存活对象复制到另外一块 3.已经用过得内存空间一次性清理掉 | 1. 扫描效率高 2.不产生内存碎片 | 费额外的内存作为复制区 | 回收新生代 |
标记整理算法 | 1. 标记需要回收的对象 2. 存活对象向一端移动 3. 清理掉端边界以外内存 | 1.不产生内存碎片 | 整理比较耗时 | 回收老年代 |
分代收集算法
分代收集(Generational Collecting),当前的商业虚拟机的垃圾收集都采用分代收集算法,根据对象不同的存活周期,将内存划分为几块。
一般是把java堆划分为新生代和老年代,根据各代的特点采用适合自己的垃圾收集算法。
堆内存分代概念:
新生代分为3个区,一个Eden区,两个Survivor区, Eden:From:To=8:1:1(默认情况下),10%的空间被浪费。
-
新生对象在Eden区生成,当Eden区满时,还存活的对象被复制到一个Survivor区
-
当这个Survivor区满时,此区的存活的对象复制到另外一个Survivor区
-
当另外一个Survivor也满时,从第一个Survivor区复制过来并且存活的对象,被复制到老年代
-
2个Survivor完全对称,轮流替换
-
可通过-XX:SurvivorRatio=x 来设置Eden区域与Survivor区域的比值,默认为8
GC时机
GC类型 | GC范围 | 触发时机 | 特点 |
---|---|---|---|
Minor GC | 新生代 | 新对象生成时Eden区满 | 执行时间短 |
Full GC | 新生代,老年代 | 1)Old满了 2)Perm满了 3)System.gc() | 效率低,应尽量减少 |
垃圾收集器
并行&并发
并行(Parallel): 指的是多个收集器的线程同时工作,但是用户线程处于等待状态
并发(Concurrent):指的是收集器在工作的同时,可以允许用户线程操作
Serial收集器
单线程收集器 ,收集时会暂停所有工作线程(Stop the world),没有多线程切换的额外开销
新生代采用复制算法;老年代采用标记-整理算法
虚拟机运行在Client模式时默认的新生代收集器
ParNew收集器
多线程收集器, 它是Serial的多线程版本,除了使用多个收集线程外,其余行为和Serial收集器一样
-XX:ParallelGCThreads控制GC线程数的多少
虚拟机运行在Server模式时默认的新生代收集器
Parallel Scavenge收集器
多线程收集器,以吞吐量最大化(GC时间占总运行时间最小)为目标,允许较长时间的STW换取总吞吐量最大化。
吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
使用复制算法
Serial Old收集器
单线程收集器,使用标记-整理算法,时老年代的收集器
Parallel Old收集器
多线程收集器,老年代版本吞吐量优先的收集器
使用标记-整理算法
sart from JDK1.6
CMS收集器
CMS: Concurrent Mark Sweep, 并发标记清除。
枚举根节点
当执行系统停顿下来后,并不需要一个不漏的检查完所有的执行上下文和全局的引用位置,虚拟机应当时有办法直接得知哪些地方存放着对象的引用。在HotSpot实现中,是使用一组称为OopMap的数据结构来达到这个目的。
安全点
安全点,Safepoint。
在OopMap的协助下,HotSpot可以
程序执行时并非在所有地方都能停顿下来开始GC,只有在达到安全点时才能暂停。
安全点选定不能太少,否则GC等待时间太长。也不能过于频繁,导致过分增大运行时的负载。