1. JVM运行时内存区域
1.1 程序计数器
线程私有,记录下条指令的位置。
1.2 虚拟机栈
每当方法调用时就会往虚拟机栈中压入栈帧,其中包含参数,局部变量,返回地址等信息。线程私有。
1.3 本地方法栈
与虚拟机栈类似,只不过虚拟机栈中为字节码服务,而本地方法栈为本地方法服务。线程私有。
1.4 Java堆
几乎所有对象都在这里,这里也是垃圾收集器管理的地方。
1.5 jdk8之前方法区
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。
1.6 jdk8开始元空间
元空间在本地内存中,字符串常量,静态变量转移到Java堆中。
2. JAVA垃圾收集器
2.1 如何确定对象应该回收
2.1.1 引用计数法
为每一个对象维护一个引用计数器,每当有新的引用指向该对象时引用计数器加一,当有引用指向其他对象时(包括null)计数器减一,当引用计数器为零时,意味着用户已经找不到该对象。此时该对象已死亡,就可以回收了。
2.1.2 可达性分析算法
引用计数法存在问题,当对象出现环形引用时,对象已死但无法发现。可达性分析算法会选择一些对象作为GC Roots。JVM会从GC Roots开始根据引用关系向下搜索,可以被搜索到的对象为存活对象,其他的均为已死亡对象。
2.2 垃圾收集算法
2.2.1 分代收集理论
分代假说:
1)弱分代假说:绝大多数对象都是朝生熄灭的。
2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
2.2.2 标记-清除算法
首先标记出所有存活的对象,在标记完成之后,统一回收所有未被标记的对象。执行效率不稳定,产生大量碎片。
2.2.3 标记-复制算法
将内存分为两部分,当一部分内存用完时将这一部分还存活的对象复制到内存的另一部分,之后回收这一部分的所有空间。运行高效且不会产生碎片,但是内存利用率低,并且当大量对象存活时运行效率将会很低。
2.2.4 标记-整理算法
与标记-清除算法类似,只不过在清除的同时会移动对象消除空间碎片。不会产生空间碎片,但是运行效率低,时间长。
2.2.5 根节点枚举(通过GC Roots做可达性分析)
在给节点枚举时需要STW(Stop The World)停止所用用户线程。以保证可达性分析的准确性。
2.2.6 并发的可达性分析
垃圾收集器遍历三色图:
- 白色:对象未被访问过。
- 黑色:对象已被访问过,且该对象的所有引用均已被扫描过。
- 灰色:对象已被访问过,但该对象至少有一个引用未被访问过。
并发可达性分析出现问题的原因(均满足才会出现):
1)赋值器插入了一条或多条从黑色对象到白色对象的新引用。
2)赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
解决方法:
1)增量更新(破坏第一个条件),当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
2)原始快照(破坏第二个条件),当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
2.3 垃圾收集器
2.3.1 Serial收集器
针对新生代,采用复制算法,单线程收集,进行垃圾收集时,必须暂停所有工作线程,直到完成。
2.3.2 ParNew收集器
ParNew垃圾收集器是 Serial收集器的多线程版本。
2.3.3 Parallel Scavenge收集器
与 ParNew收集器类似是新生代收集器,采用复制算法,多线程收集。Parallel Scavenge收集器的目标则是 达一个可控制的吞吐量。
2.3.4 Serial Old收集器
Serial Old 是 Serial收集器的老年代版本,单线程。与Parallel Scavenge搭配使用或作为CMS失败后的后背预案。
2.3.5 Parallel Old收集器
Parallel Old 是 Parallel Scavenge收集器的老年代版本。JDK6时才开始提供。
2.3.6 CMS收集器
- CMS(Concurrent Mark Sweep)收集器,以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。
- CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:
1、初始标记(CMS initial mark)。
2、并发标记(CMS concurrent mark)。
3、重新标记(CMS remark)。
4、并发清除(CMS concurrent sweep)。
注意:“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。 - 其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
- 初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记阶段就是从GC Root的直接关联对象开始遍历的过程。这个过程时间长,但是可以和用户线程并发执行。
- 重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的。
2.3.7 Garbage First(G1)垃圾收集器
将整个Java堆划分为多个大小相等的独立区域(Region),每个分区也不会确定为某个代(年轻代、老年代)服务,可以按需在年轻代和老年代之间切换。
2.4 长期存活的对象将进入老年代
-
虚拟机采用分代收集的思想来管理内存,内存回收时必须识别哪些对象放入新生代,哪些对象放入老年代。为了做到这点,虚拟机为每个对象定义了一个对象年龄计数器。
-
如果对象在Eden出生并经过一次Minor GC仍然存活,并且能被Survivor容纳,将被移动到Survivor区,并且对象年龄设置为1.对象每经过一次Minor GC后仍保持存活,年龄+1
-
当对象年龄到达一定程度(一般15岁),那么它会晋升到老年代。对象晋升的年龄限制 -XX:MaxTenuringThreshold设定
2.4 空间分配担保:
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。直接Minor GC。若条件不成立,则查看是否允许担保失败,若不允许担保失败,则直接进行Full GC。如果允许会先检查老年代最大连续空间是否大于以往每一次回收晋升到老年代对象空间大小的平均值,如果小于,则进行一次Full GC。如果大于就开始Minor GC,当大量对象在Minor GC后仍然存活,就需要老年代进行空间分配担保,把Survivor无法容纳的对象直接进入老年代,此时若老年代空间不足以担保时(此时是因为实际晋升到老年代的对象远大于以往每一次回收晋升到老年代对象空间的平均值),则会进行一次Full GC。