概述
1.判断对象存活
JVM中盘判断对象存活有两种方法:引用计数法和可达性分析。
1.1引用计数法
每个对象都有一个引用计数器,被引用时加1,引用失效时减一。
优点:快,方便,实现简单。
缺点:对象相互引用时,很难判断对象是否改回收。
1.2可达性分析
通过一系列成为根对象“GC Roots”的节点开始向下搜索,搜索经过的路径称为引用链(Reference Chain)。当一个对象到根节点没有任何引用链时,代表对象不可用。
可以作为GC Roots的对象有:
- 虚拟机栈(栈针的局部变量表)中引用的对象
- 方法区中静态中类静态属性所引用的对象
- 方法区中常量所引用的对象
- 本地方法栈中所引用的对象
2.各种引用
2.1强引用
我们平时new对象就是强引用。
2.2软引用SoftReference
有用但非必须的,用软引用关联的对象,在发生OOM之前,就会被回收。
2.3弱引用WeakReference
有用但非必须,比软引用更弱,在下一次垃圾回收之前,GC时会被回收。短时间内通过弱引用取对应的数据,可以取到。垃圾回收到就收不到了。
2.4虚引用PhantomReference
幽灵引用,最弱,可以用来被垃圾回收的时候收到一个通知。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。
3.垃圾回收算法
3.1标记-清除算法(Mark-Sweep)
标记-清除算法分为标记和清除两个阶段。首先标记处需要回收的对象,然后在标记完成后统一回收。
缺点:经历标记和清除两个阶段后,容易产生大量不连续的内存碎片。这样会导致在需要分配较大内存空间时,无法找到足够的连续内存空间,而不得不出发下一次垃圾回收。
3.2复制算法(Copying)
复制算法是将内存分为大小相等的两块内存,每次只使用其中一块,当这一块使用完后,就将活着的对象复制到另外一块内存中,再将原有一块内存全部清理。
优点:实现简单,没有内存碎片的复杂情况。
缺点:可以使用的内存只有原有的一半,资源利用率不好。
3.3标记-整理算法(Copying)
标记整理实际上是融合了标记-清除和复制两种算法的思想。和标记-整理算法一样也是分为两个阶段。第一个阶段同样是标记需要回收的对象。第二个阶段不是直接回收,而是将活的对象移动到一侧,然后直接清理掉边界以外的区域。
优点:降低了内存碎片,并且内存资源利用率高。
缺点:实现复杂。
4.分代算法
目前商业虚拟机对垃圾回收采用的都是分代算法。根据对象不同的存活周期,将堆划分为了新生代和老年代。然后采用不同的算法,一半新生代采用复制算法。老年代采用标记整理。
4.1新分代
(1)根据研究表明,新生代的对象98%都是朝生夕死。所以新生代的内存分为一块较大的Eden空间和两块较小的Survivor空间。每次使用的都是Eden和其中一块Survivor区域。发生minor gc时,将存活对象放入另外一块Survivor区域。原来Eden和其中一块Survivor区域直接清除,再加上新生代存活的对象很少,所以采用复制算法。
(2)minor gc
- minor gc就是发生在新生代的GC。在最开始的时候,对象是创建在Eden区,两块Survivor区都是空的。发生第一次minor gc后,存活的对象会被移动到其中一块Survivor区,假设为Survivor0。这时候Survivor0里面对象的年龄为1。下一次GC时,Eden区存活对象和Survivor0中存在对象移动到Survivor1。原有Survivor0中存活对象年龄加1变为2。默认在新生代年龄达到15后,下一次minor gc还存活,要晋升到老年代。
- 这里还要说一下另外一个概念:分配担保。在发生minor gc时,需要判断老年代剩余空间是否大于新生代存活对象的大小。如果大于,进行minor gc。如果不大于,JVM会查看是否允许担保失败HandlePromotionFailure。如果允许,则会判断老年代大小是否大于历次晋升的平均大小。如果大于,进行minor gc。如果不大于或者不允许担保失败,则进行full GC。
(3)minor gc和full gc出发条件
- minor gc:Eden区满了
- FULL GC:老年代满了、永久代满了、调用System.gc时,系统建议执行Full GC,但是不必然执行、通过Minor GC后进入老年代的平均大小大于老年代的可用内存、由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
这里再说下永久代满了时候对永久代的清理:
永久代包括常量、类信息。无用的类信息回收时需要满足三个条件:类的实例已经被回收、类的加载器已经被回收、类的CLASS对象已经无引用。
(4)配置
新生代大小配置参数的优先级:
高:-XX:NewSize/MaxNewSize
中间 -Xmn (NewSize= MaxNewSize)
低:-XX:NewRatio 表示比例,例如=2,表示 新生代:老年代 = 1:2
-XX:SurvivorRatio 表示Eden和Survivor的比值,
缺省为8 表示 Eden:FromSurvivor:ToSurvivor= 8:1:1
4.2老年代
老年代因为回收时会有大量对象回收,也没有额外空间对它进行分配担保,所以采用使用“标记—清理”或者“标记—整理”算法来回收。
大对象直接进入老年代:需要大量连续空间的java对象,比如很长的字符串或者大型数组。导致:
- 内存有空间,还是需要提前进行垃圾回收来获得连续空间。
- 会进行大量内存复制。
-XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。
长期存活的对象将进入老年代,默认15岁,-XX:MaxTenuringThreshold调整
动态对象年龄判断:虚拟机并不是永远要求对象的年龄到达MaxTenuringThreshold才能晋升老年代。如果在Survivor区相同年龄的对象空间大小总和大于Survivor空间大小的一半,年龄大于等于该年龄的对象可以直接进入老年代,无需等到年龄到达MaxTenuringThreshold。
5.垃圾回收器
先说下垃圾回收器里面并行和并发的概念。
并行:多个线程进行垃圾回收。
并发:垃圾回收器线程和业务程序线程同时运行。
5.1垃圾回收器列表
(1)Serial
Serial垃圾回收器,使用在新生代、单线程、复制算法。因为单线程,所以工作时需要暂停工作线程。适合单CPU 服务器。
-XX:+UseSerialGC 新生代和老年代都用串行收集器。
(2)ParNew
ParNew使用在新生代,多线程并行、复制算法、还是需要暂停工作线程。多线程,多CPU的,停顿时间比Serial少。
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old。
(3)Parallel Scavenge
关注吞吐量的垃圾回收器。使用在新生代,多线程并行、复制算法、还是需要暂停工作线程。高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
吞吐量=工作线程执行时间/(工作线程执行时间+垃圾回收时间)。垃圾回收时间=垃圾回收次数*单词垃圾回收时间。
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old。
-XX:MaxGCPauseMills :参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。
-XX:+UseAdaptiveSizePolicy 当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。
(4)Serial Old
使用在老年代,单线程,标记整理算法。jak7和jdk8老年代默认的回收器。
(5)Parallel Old
使用在老年代,并行多线程,标记整理算法。Parallel Scavenge老年代的版本,为搭配Parallel Scavenge高吞吐量而开发的。
(6)CMS
使用在老年代,并行和并发混合使用,标记清除算法。为尽可能减少停顿时间,适合重视响应时间的系统。不过内存碎片多、需要较高的CPU资源。
垃圾回收分为四个阶段:
- 单线程初始标记,只标记根节点GC Roots能直接到达的地方。
- 并发标记:不需要停顿工作现场,这时候进行GC RootsTracing的过程。
- 重新标记:并行多线程重新标记,需要停顿工作现场。为了修正在并发标记阶段因工作线程变动而变化的标记,这个阶段比初始标记长,但远低于并发标记。
- 并发清理:不停止工作现场清理对象。
由于最耗时间的并发清理和并发标记都不停止工作线程,所以适合对用户响应有要求的服务。
-XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用CMS
(7)GI
使用在新生代和老年代,并行和并发混合,采用标记整理和化整为零。采用分区回收,在不牺牲吞吐量的情况下,完成低成本内存回收。并且停顿可预测。
-XX:+UseG1GC
上述回收器搭配如图:
(8)ZGC
JDK11中的垃圾回收器,GC时间不超过10ms,与G1相比,吞吐量降低不超过15%。
6.内存泄漏和内存溢出
内存溢出:实实在在的内存空间不足导致;
内存泄漏:该释放的对象没有释放,多见于自己使用容器保存元素的情况下。
7.JDK工具
- JPS:查看虚拟机进程
- jstat:监视虚拟机各种运行状态信息的命令行工具
- jinfo:java配置信息工具类
- jmap:java内存映射工具类
- jstack:java堆栈跟踪工具类
- jConsole:java监视管理控制台
- VisualVM:多合一故障处理工具类
8.深堆和浅堆
(1)浅堆
浅堆 :(Shallow Heap)是指一个对象所消耗的内存。例如,在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。
(2)深堆
深堆 :这个对象被GC回收后,可以真实释放的内存大小,也就是只能通过对象被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象所持有的对象的集合。深堆是指对象的保留集中所有的对象的浅堆大小之和。
举例:对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。
最后
以上就是开放铃铛为你收集整理的JVM调优系列5-垃圾收集器的全部内容,希望文章能够帮你解决JVM调优系列5-垃圾收集器所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复