文章目录
引言
Java和C++最大的不同点就是Java能够自动回收内存,而Java垃圾回收虽然程序员一般不用去管,但是也要知道其中的道道,一下是我在看《深入理解JVM》的一些小小的心得,不一定正确,有错误欢迎大家指正!
首先我们要知道Java垃圾回收机制做了一件什么事情?
Java垃圾回收机制就是对内存中不需要的对象(主要是对象),和类进行回收
Java垃圾回收机制主要解决了什么问题?
-
哪些对象要回收?
引用计数器法
可达性分析法 -
生存还是死亡?
finalize()方法的执行
-
怎么回收这些对象?
- 标记清除算法
- 标记复制算法
- 标记整理算法
-
七种垃圾收集器
5.什么时候回收这些对象?
分新生代、老年代的GC,Minor GC和Full GC
1、哪些对象要回收?
这个关系到一个对象生存还是死亡的重要依据了。那么JVM有两种判断对象是否要继续存活的方法:
- 引用计数器法
- 可达性分析法
(1)引用计数器法
所谓引用计数器法,就是说每个对象都有一个计数器,
当一个对象被另外一个对象引用的时候,这个计数器就会+1,
当引用断开的时候引用计数器就会-1.
当一个对象的引用计数器的值为0的时候,就说明这个对象是可以被回收的
优点:实现起来简单执行效率高,程序执行受影响小
缺点:无法解决两个对象相互引用的问题
(2)可达性分析法
可达性分析法顾名思义,就是从某一个点,看看有没有一条存在的路径到达对象。
如果存在的话,则说明这个对象是我们需要的,还需要使用的
如果不存在的话,则说明这个对象我们已经用不上了,JVM可以回收掉了
可达性分析法就可以解决掉引用计数器法无法解决的问题,
当两个对象相互引用而没有第三方引用的的时候,找不到一条路径到达这两个对象
那么我们怎么定义一条路径开始的端点,也就是开始的起点呢?
JVM定义了一些GC Root,分为一下几种:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(native)引用的对象
- 被同步锁(synchronized)持有的对象
在上面我们一直在说引用,那到底什么是引用?引用分为哪几种呢?
引用分为四种:
- 强引用:
也就是用new关键字生成的对象,形如:Object object = new Object();
这种引用是属于强引用,也就是JVM无论如何都不会回收掉的对象,除非显式的将
object置为null :object = null;
- 软引用:
SoftReference<Object> object
= new SoftReference<Object>(new Object());
是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,
只有当JVM认为内存不足时,才会去试图回收软引用指向的对象。
JVM会确保在抛出OutOfMemoryError之前,清理软引用指向的对象。
- 弱引用:
WeakReference<Object> object
= new WeakReference<Object>(new Object());
被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用:
ReferenceQueue<String> referenceQueue
= new ReferenceQueue<String>();
PhantomReference<String> object
= new PhantomReference<String>(new String(),
referenceQueue);
它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间
构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的
唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
2、生存还是死亡?
不是说再被可达性分析法标记为不可达的对象就一定要被回收的,换句话说不是非死不可
就像以往当地官员想要斩杀某一个人的时候,可以有一个皇帝谕旨来拯救这个人。但是在
JVM里面,皇帝谕旨只能起作用一次,在JVM里面,皇帝的谕旨称之为finalize()方法。
生存还是死亡的完整过程:
首先通过可达性分析法判断是否有一条路径达到这个对象,如果存在的话,
说明是可以存在。否则的话,进行第一次标记。随后就会进行一次筛选,筛选的条件
就是对象是否有必要执行finalize()方法。
如果对象没有覆盖finalize()方法的话,
就会被直接回收掉
如果对象覆盖了finalize()方法的话,
判断当前对象有没有执行过finalize()方法
如果当前对象已经执行了一次了,则finalize()方法没有必要再执行,直接回收
如果当前对象没有执行过,则可以再finalize()让自己重新被引用从而拯救自己
而这里被判断有必要执行finalize()方法之后,并不是立即执行的,将会把这个对象放置在
一个名叫F-Queue的队列中,然后会由虚拟机自动建立一条低调度优先级的Finalizer线程
去执行他们的finalize()方法。
这里的执行只是说会执行,并不会等待结束。防止执行finalize()方法的时间过长,
或者陷入死循环导致程序坏死。
3、怎么回收这些对象?
(1)分代收集理论
-
弱分代假说:对大多数的对象都是朝生夕灭的
-
强分代假说:熬过越多次垃圾收集过程的对象约难以消亡
所以收集器应该将Java对划分出不同的区域,然后根据年龄分配到不同的存储区域中
划分区域之后,就诞生出了一系列的回收类型的划分
minor GC – 标记复制算法 – 针对新生代的垃圾收集
major GC – 标记清除算法 – 针对老年代的垃圾收集
full GC – 标记整理算法 – 针对整个Java堆和方法区的垃圾收集
(2)标记-清除
顾名思义,就是标记之后直接清除。
优点:
实现起来简单
缺点:
执行效率不稳定
如果Java堆中包含大量对象,而其中大部分都需要被回收,
此时就会导致大量的标记和清除动作
产生大量碎片
产生大量碎片之后,大对象无法分配就会频繁的产生GC
(3)标记-复制
提供了一种半区复制的思想,把存活的对象复制到另一半
但是这样空间浪费太大了,就提出了Eden :Survivor from :Survivor to =8:1:1
在发生垃圾收集的时候,将Eden和Survivor中仍然存活的对象复制到另一块Survivor区域
如果Survivor没有足够空间来存放上次新生代GC存活下来的对象,
它们将通过分配担保机制进入老年代
优点:
解决了内存碎片的问题
缺点:
对象存活率高得时候,复制效率低
(4)标记-整理
先执行标记-清除,让所有的存活对象都向一端移动,最后清理掉边界以外的内存
(5)垃圾回收器
1.新生代
Serial/Serial Old收集器运行示意图:
ParNew/Serial Old收集器运行示意图:
-
Serial – 标记-复制算法
特点:单线程收集(强调在进行垃圾收集的时候,必须暂停其他所有工作线程,直到它结束);
场景:Client模式下默认新生代收集器;单核机器 -
ParNew – 标记-复制算法
特点:多线程并行收集;其他特点与Serial相似
缺点:在单CPU场景效果不突出
场景:用户交互;配合CMS垃圾收集器 -
Parallel Scavenge – 标记-复制算法
特点:目标在于达到可控吞吐量(吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间));
场景:高效利用CPU,后台运算且不需要太多交互
2.老年代
Serial/Serial Old收集器运行示意图:
ParNew/Serial Old收集器运行示意图:
Parallel Scavenge/Parallel Old收集器运行示意图:
-
Serial Old – 标记-整理算法
特点:Serial的老年代版本,单线程;
场景:1.5之前与Parallel
Scavenge配合使用;作为CMS的后备预案 -
Parallel Old – 标记-整理算法
特点:多线程;是parallel scavenge收集器的老年代版本
场景:为了替代Serial Old与Parallel Scavenge配合使用 -
CMS – 标记-清除算法
特点:最短回收停顿时间;
步骤: 初始标记和重新标记会stop
初始标记:标记GC Roots直接关联的对象,速度快 并发标记:GC Roots
Tracing过程,耗时长,与用户进程并发工作
重新标记:修正并发标记期间用户进程继续运行而产生变化的标记,耗时比初始标记长,但远小于并发标记 并发清除:清除标记的对象
缺点 对CPU资源敏感 无法回收浮动垃圾 标记-清除算法,会产生内存碎片,可以通过参数开启碎片的合并整理
3.G1
G1
特点:
将整个Java堆划分为多个大小相等的独立区域Region,跟踪各个Region里面的垃圾
堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价
值最大的Region
步骤
初始标记 最终标记 筛选回收都会stop
初始标记:标记GC Roots直接关联的对象
并发标记:对堆中对象进行可达性分析,找出存活对象,耗时长,与用户进程并发工作
最终标记:修正并发标记期间用户进程继续运行而产生变化的标记
筛选回收:对各个Region的回收价值排序,然后根据期望的GC停顿时间制定回收计划
4、什么时候回收掉这些对象?
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存 以及 回收分配给对象的内存。一般而言,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB),将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中。总的来说,内存分配规则并不是一层不变的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
- 对象优先在Eden分配,当Eden区没有足够的精简进行分配的时候,虚拟机将发起一次Minor GC
我们在上面讲了,根据弱分代假说和强分代假说,都支持把Java堆分成几个区域,分别进行GC。 其中Minor
GC发送在新生代的GC,主要采用标记复制算法。而由于标记复制算法开始是使用半区
复制的算法,这样会导致空间利用不充分,于是涉及了Eden:Survivor from:Survivor to =
8:1:1,兑现先在Eden区分配,等Eden区没有足够空间分配的时候,虚拟机将会发起一次
Minor
GC 把Eden区和Survivor from 区的存活的对象往Survivor to区复制,然后清除剩下的区域 - 大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的就是数组了 - 长期存活的对象直接进入老年代
当新生代中每经历一次Minor GC,年龄+1。当年龄达到15(默认)的时候,就会被晋升到老年代 - 动态对象年龄判定。
为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold
才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,
年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 - 空间分配担保
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,
如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看
-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);
如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,
如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;
如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次FullGC。
5、其他
Java GC机制虽然主要针对对象,但是不单单针对对象。还针对方法区的回收,主要涉及 常量池的回收 和 堆类型的卸载。
1、常量池的回收
回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,
假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,
换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,
如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。
常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
2、类的卸载
其实就是类的回收,那么判断无用类要满足一下几种条件
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
2)加载该类的ClassLoader已经被回收;
3)该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
对一个类的判断条件还是比较苛刻的,所以一般来说不会去回收类
虽然JVM的新生代的划分为 8 1 1原则,但是我们可以通过参数去更改,下面我们来看一下一些具体的参数
Heap(堆)内存大小设置
-Xms512m
设置JVM堆初始内存为512M
-Xmx1g
设置JVM堆最大可用内存为1G
新生代内存大小设置
-Xmn256m
设置JVM新生代内存大小(-Xmn 是将NewSize与MaxNewSize设为一致。256m),同下面两个参数
-XX:NewSize=256m
-XX:MaxNewSize=256m
通过设置新生代老年代的比值来设置新生代的大小
-XX:NewRatio=3
设置新生代(包括Eden和两个Survivor区)与老年代的比值(除去持久代)。
设置为3,则新生代与老年代所占比值为1:3,新生代占整个堆栈的1/4
Survivor内存大小设置
-XX:SurvivorRatio=8
设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个新生代的1/10
这只是我对自己所学知识的一点点小总结,如果由不正确的地方,欢迎大家指针,大家一起学习进步!!
我是小菜鸡,一个渴望成为别人眼中大佬的人