java基础7
二、垃圾回收机制(GC)
垃圾回收主要针对堆与方法区(比较少)来进行。
判断对象是否可被回收
1. 引用计数算法
为对象添加一个引用计数器,引用计数为0的对象可被回收。
存在的问题:如果两个对象出现循环引用时,无法被回收(a引用b对象,b对象引用a对象/死锁)。造成内存泄露。
2. 可达性分析算法
以GC Root为起点进行搜索,不可达的对象可被回收。
通过使用Eclipse Memory Analyzer,得到的有 系统类、本地方法栈、锁对象和活动线程。
具体来说,GC Root内容包括:
- 虚拟机栈(栈帧中的局部变量区)中引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象;
- 方法区中(PS部分)类静态属性引用的对象;
- 方法区中的常量引用的对象。
PS:在JDK1.8版本中,串池与静态变量都存放至堆中,便于垃圾回收。
Java的引用类型
1. 强引用
被强引用关联的对象不 会被回收。(即使出现OOM也不会被回收)
使用 new 关键字来创建强引用。
2. 软引用
被引用关联的对象只有在内存不够的情况下才会被回收(与该引用对象得到的时间,当前堆可用内存大小都有关系)。(可以配合引用队列来释放该引用的空间占用)
使用 SoftReference类来创建软引用。
Object ojb = new Object();
SoftReference<object> w = new SoftReference<object>(obj);
obj = null
// 软引用案例 使用 -Xmx20m -XX:+PrintGCDetails -verbose:gc
private static final int _4MB = 4*1024*1024;
public static void main(String[] args){
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i=0;i<5;i++){
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 使用软引用,前面的3条都会被回收掉
for(SoftReference<byte[]> ref : list)
System.out.println(ref.get());
}
应用
如果有一个应用需要读取大量的本地图片:
- 如果每次读取图片都需要从硬盘读取的话,会严重影响性能。
- 如果一次性加载到内存则会造成内存溢出。
针对该情况,可以设计一个HashMap<String, SoftReference> 来存储路径字符串与图片对象关联的软引用之间的映射关系,内存不足时,JVM会自动回收图片对象占用的空间,有效避免了OOM问题。
3. 弱引用
被弱引用关联的对象只能存活到下一次垃圾回收发生之前。
(实际中如果空间不够则会回收最近的弱引用所引用的对象)
(可以配合引用队列来释放该引用的空间占用)
使用 WeakReference类进行弱引用的创建。
WeakHashMap
当key不再被使用(置null)时,其对应的Entry对象会被垃圾回收掉。
4. 虚引用
一个对象的虚引用的存在与否不会对原始对象的生存时间造成影响,也无法通过虚引用来得到一个对象。(在JVM对直接内存的回收ByteBuffer时有应用)
虚引用引用的对象被垃圾回收时,虚引用对象自身会被放入引用队列。由其它线程(保护线程ReferenceHandler)去调用虚引用对象的方法。
使用PhantomReference来创建虚引用。
5. 终结器引用(不推荐)
当对象重写了终结方法 finalize() ,并且没有强引用对该类进行引用时,虚拟机会创建一个对象的终结器引用。
当该对象要被回收时,终结器引用先被放入引用队列,由一个优先级很低的线程来查看待回收对象中是否包含终结器引用,如果有,则调用对象的finalize() 方法,等下一次垃圾回收时才会被回收。
JVM常见错误/异常
-
StackOverflowError
-
OutOfMemoryError: Java heap space
-
OutOfMemoryError: GC overhead limit exceeded(JVM垃圾回收时间过长)
-
OutOfMemoryError: Direct buffer memory(直接内存溢出,DirectByteBuffer)
-
OutOfMemoryError: unable to create new native thread(高并发请求服务器时易出现)
原因: 一个应用进程创建多个线程,超过系统承载极限(linux默认是1024个,一般上限为默认值的2/3已经很高了) -
OutOfMemoryError: Metaspace
垃圾回收算法
标记清除(老年代)
先标记要回收的对象,然后进行统一回收。
优点:处理速度较快。
缺点:易产生内存碎片。
标记整理(老年代)
先标记要回收的对象,然后再次扫描,往一端移动存活对象。
优点:不会产生内存碎片;
缺点:处理速度较慢。
复制(新生代)
只有一个阶段,将内存区划分为大小相等的两个区域,一个区域空闲(TO区),一个被使用(FROM);在进行垃圾回收时,会将FROM中使用的空间放入TO中,同时不会产生碎片,在交换完成后,交换FROM与TO区域(指针交换)。
优点:不会有内存碎片;
缺点:需要占用双倍的内存空间。
分代的垃圾回收机制(Minor gc)
- 复制
首先,Eden区满了后会触发第一次gc,此时会把还存活的对象拷贝到SurvivorFrom区;当Eden再次满了时,则会对Eden和SurvivorFrom两个区域进行垃圾回收,此时还存活的对象会被复制到SurvivorTo区,同时这些对象年龄+1。(如果对象年龄超过阈值 [阈值由JVM参数 MaxTenuringThreshold 决定,默认15] 或对象过大,会被放入老年代) - 清空
进行复制操作后,Eden和SurvivorFrom区会被清空。 - 互换
最后,SurvivorFrom与SurvivorTo区域进行交换。
PS:一个线程内的OOM(OutofMemoryError)并不会导致主线程的结束。
垃圾回收器
对应的连线表示新生代与老年代垃圾回收器的搭配方式。
-
串行(Serial, 1:1):单线程,适用于堆内存较小,单核CPU。(新生代)
-
ParNew(N:1):新生代使用并行垃圾回收器,老年代默认使用SerialOldGC。但是目前最常见的是配合老年代的CMS工作。
新生代使用的是复制算法;
老年代使用的是标记-整理算法;
-
Parallel Scavenge(N:N):新生代老年代都使用ParallelGC。高吞吐量(利用CPU的时间在总时间的占比),同时采用自适应调节策略动态调整参数。
-
响应时间优先(CMS,并发标记清除):多线程,可以与用户线程并发执行,适用于堆内存较大,多核CPU。让单次的STW时间最短。(用于老年代)
存在的问题:由于采用的是标记清除算法,因此垃圾回收时会产生很多内存碎片,导致并发失败,退化为串行垃圾回收,造成了垃圾回收时间较长。
CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败。
一般使用 ParNew(新生代)+CMS(老年代)+SerialOld(用于CMS出错时的后备收集器)组合。
G1(Garbage First)
堆内存会被划分为很多固定大小的区域,每个区域分别被标记为E(伊甸园),S(幸存者),O(老年代)和H(巨型区域,存放的对象大小大于等于区域的一半)。(区域不再连续)
优点
- 使用的是内存整理算法,不会产生太多内存碎片。
- STW更可控,用户可以指定期望停顿的时间。
- 区域化内存划分,避免了全内存区的GC操作。
适用场景
- 同时注重吞吐量与低延迟,默认暂停目标是200ms。
- 适用于超大堆内存,会将堆划分为多个大小相等的区域。(region)
- 整体上为标记-整理算法,区域之间使用的是复制算法。
相关JVM参数
-XX:+UseG1GC // JDK9默认打开
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
回收流程(类似CMS)
- 初始标记(STW):标记直接可达对象(根对象);
- 并发标记:寻找存活对象;(老年代空间比例达到阈值时发生)
- 最终标记:标记并发标记阶段发生变化的对象,用于回收;
- 筛选回收:对各区域回收价值进行评估, 使用Young GC或Mixed GC进行垃圾回收。(CMS使用的是标记清除)
Young GC
每次回收所有的伊甸园区与幸存者区,并将存活对象复制到老年代以及另外一部分幸存者区。
Mixed GC
除了回收整个新生代,还会回收部分老年代,并且可以进行选择性回收(选择回收价值最高的进行回收)。
当垃圾回收速度跟不上垃圾产生速度时,退化为串行垃圾回收(进行 full gc)。
JVMGC与SpringBoot微服务生产部署与调参优化
java -server [parameters] -jar package.jar/war
parameters中使用JVMGC参数进行调优。