一. JVM内存分配和回收
Java中内存自动管理主要是针对对象的内存分配和对象的内存回收,其中最主要的的是堆的管理。
1. 堆内存划分
从垃圾回收的角度,现在主流的垃圾收集器都将堆分为新生代和老年代,再细分就分为Eden、From Survivor、To Survivor等空间。
- 堆内常见的内存分配策略
- 对象优先在Eden区分配
- 大对象在老年代分配,避免频繁复制导致效率低下,如大数组、字符串等
- 长期存活的对象在老年代分配,通过年龄判断,一般默认为15
2. GC流程
大部分对象都会首先在Eden区域分配内存,当Eden空间不足时就会触发Minor GC。
- 执行一次新生代垃圾回收,如果对象还存活,就将对象加到s1(To Survivor),并且将对象的年龄加1(当对象的年龄到达阈值的时候就会加入老年代)。
- 经过一次GC后,Eden和s0(From Survivor)就被清空了,因为存活的对象都加入到s1了
- 然后将s0与s1调换,也就是说把上一次的"From"变为"To",这样就保证了To空间始终是空的,这也说明了"From"和"To"只是逻辑上的划分,当"To"空间被填满时,就将对象加入老年代中。
新生代 GC(Minor GC):发送在新生代内存区域,频繁,速度快
老年代 GC(Full GC/Major GC):发送在老年代内存区域,速度慢
二. 判断对象是否死亡
1. 引用计数法
给对象添加一个引用计数器,当有对象引用时就加1,引用失效时就减1,引用计数器为0时,则表示对象死亡。
这种方式简单、高效,但不能解决循环引用的问题。
循环引用:A对象拥有B对象的引用,B对象也拥有A对象的引用,此时A、B对象的引用计数器都不可能为0,所以它们不可能被回收。
2. 可达性分析算法
这个算法通过一系列称为"GC Root"的对象作为起点(通过某些方法决定哪些对象作为"GC Root"对象),其他对象都直接或间接的引用了这些"GC Root"对象,从上往下搜索,搜索的路径叫做引用链,当一个对象到"GC Root"对象没有任何一条引用链的话,则表明这个对象是无用的,也就是说该对象与"GC Root"没有直接或间接的引用关系。
- 不可达的对象不一定会被回收
对象被判定为不可达后,仍要经历一次或两次标记才会被回收。
第一次标记:判断对象是否覆盖finalize()方法,如果有覆盖则进入第二次标记,如果没有则对象被回收。
第二次标记:创建一个低优先级的线程,在线程中执行对象重写的finalize()方法(不一定执行完,避免无限循环等情况使线程无限等待),如果finalize()方法执行完后,对象仍然为不可达则对象被回收。
3. 引用种类
强引用:使用最普遍的引用,当内存空间不足时,抛出异常也不会回收该对象。
软引用:当内存充足时,不会回收该对象,当内存不足时,就会回收该对象,可以和引用队列联合使用
弱引用:不管内存空间是否充足,都会回收该对象,可以和引用队列联合使用
虚引用:形同虚设什么时候都会被回收,可以和引用队列联合使用
引用队列(ReferenceQueue):软引用、弱引用、虚引用使用时都可以关联一个引用队列,当对象被回收时JVM会将该引用加入到引用队列中,通过查看引用队列来判断对象是否已经被回收。
4. 判断一个类是无用类
- 类的所有实例都被回收,内存中不存在任何实例
- 加载类的ClassLoader已经被回收
- 该类的Class对象没有被任何地方引用,即为不可达状态
满足这三个条件虚拟机就可以堆无用类进行回收,但不是必然会被回收。
三. 垃圾收集算法
1. 标记-清除算法
最基础的收集算法,首先标记出所有可回收的对象,在标记完成后,再统一回收被标记的对象。
此算法会导致两个明显的问题。
- 效率问题:回收时需要遍历全部对象
- 空间问题:回收完后会导致大量不连续的空间碎片
2. 复制算法
提高了效率,此算法将内存分为两块,每次使用其中一块,当这一块内存使用完后,就将其中还存活的对象复制到第二块内存中,然后再将第一块内存全部清除。
次算法的消耗是复制存活的对象,所以此算法适用于对象比较小的情况,常用于新生代内存区域。
3. 标记-整理算法
与标记-清除算法一样,先对可回收的对象进行标记,然后再将仍然存活的对象向一端移动,即将可回收的对象覆盖掉,最后再清除边界外的内存。
4. 分代收集算法
这种算法会将堆内存分为几份,根据对象的生存周期而使用不同的收集算法。一般会将堆内存分为新生代和老年代,由于新生代会有大量的对象死亡,所以适合用复制算法只需要消耗少量的复制成本,而老年代中的对象一般不易死亡并且数据量较大,所以适合用标记-整理算法或者标记-清除算法。
四. 垃圾收集器
垃圾收集器常见的有五种,目前没有最好的垃圾收集器,不同的场景适用不同的垃圾收集器。
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- CMS收集器
- G1收集器
1. Serial收集器
最基本、最悠久的收集器。
- Serial收集器是单线程的,并且在垃圾收集的时候会暂停其他线程,直到收集结束。
- 新生代使用的是复制算法,老年代使用的是标记-整理算法
目前还么有产生能够避免回收过程所产生的停顿,只是不同的收集器产生的停顿时间长短不同。
2. CMS收集器
CMS收集器是HotSpot虚拟机真正意义上的并发收集器,第一次实现了垃圾收集与用户线程(基本上)同时工作。
CMS是基于标记-清除算法的垃圾收集器,主要分为四个步骤。
- **初始标记:**这个过程会暂停所有其他线程,并标记出与Root直接相连的对象,即找出引用链的起点。
- **并发标记:**这个过程会与用户线程并发执行,通过初始标记出的对象去标记所有可达的对象,即标记出所有在引用链中的对象。
- **重新标记:**因为并发标记是与用户线程并发执行的,所以在用户线程执行的过程中可能会产生新的对象,即产出新的引用链,所以需要暂停所有线程对新产生的对象进行重新标记,避免回收了不必要回收的对象。
- **并发清除:**与用户线程并发执行,对可回收对象进行回收。
CMS收集器优点:低停顿、并发收集。
CMS收集器缺点:1. 因为使用标记-清除算法所以会产生大量空间碎片
2. 因为与用户线程并发执行,所以会占用CPU资源,导致用户线程卡顿
3. 无法处理浮动垃圾,即当CMS标记对象后,用户线程又不需要该对象了,导致该对象只能在下一次 GC中回收
3. G1收集器
G1是一款面向服务器的垃圾收集器,主要针对多颗处理器以及大容量内存的机器,极大的概率满足低停顿要求,还具备高吞吐的性能。
特点:
- 并发与并行:充分利用CPU缩短短停顿。
- 分代收集:保留了分代的概念,虽然保留了分代的概念,但是新生代和老年代不再是物理划分,也就是说不再是连续的
- 空间整合:使用标记-整理的算法,避免了空间碎片化
- 可预测停顿:让垃圾收集消耗的时间小于指定的时间
G1收集的过程和CMS类似
- 初始标记(需要停顿)
- 并发标记
- 重新标记(需要停顿)
- 筛选回收
G1收集器维护了一个优先列表,在允许的回收时间内,选择价值最大的Region(区域),使用这种方法保证了G1能够在有限的时间内回收效率尽可能的高。