垃圾回收(GC)

GC需要完成的三件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

为什么“GC自动化”之后还要研究GC?当需要排查各种内存溢出、内存泄漏问题时,当GC成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。

计数算法

package com.xiaoyu.chap3.GC;

/**
* Created by xiaoyu on 16/4/4.
*
* testGC()执行后,objA和objB会不会被GC呢?
*/
public class ReferenceCountingGC { public Object instance = null;
private static final int _1MB = 1024*1024; //搞个成员占点内存
private byte[] bigSize = new byte[2*_1MB]; public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null; System.gc();
} public static void main(String[] args) {
ReferenceCountingGC.testGC();
} } output:
[GC (Allocation Failure) 512K->440K(65024K), 0.0022170 secs]
[GC (System.gc()) 5037K->4656K(65536K), 0.0014100 secs]
[Full GC (System.gc()) 4656K->532K(65536K), 0.0074590 secs]

从输出结果上可以看出,jvm并没有因为这两个对象互相引用而不回收它们,说明用的不是计数算法。

可达性分析算法

可作为“GC Roots的对象“

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

引用

四种引用强度:

  • 强引用:类似Object obj = new Object(),只要强引用还在,GC永远不会回收。
  • 软引用:有用但非必须。必要时,第二次回收。
  • 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
  • 虚引用:存在与否对对象无任何影响,唯一目的就是在这个对象被GC时收到一个系统通知。

live or die?

要真正宣告一个对象死亡,只要要经历两次标记过程。

第一次标记:如果可达性分析后发现没有与GC Roots相连接的引用链,就会被第一次标记。

第二次标记:如果第一次标记后,对象没有必要进行finalize()方法,则被第二次标记

何为没有必要进行finalize()?

  1. 对象没有覆盖finalize()方法
  2. finalize()方法已经被虚拟机调用过

如果”被认为有必要执行finalize()方法“,那么对象会被放置在F-Queue队列中,并由一个虚拟机生成的低优先级的Finalizer线程去执行它。

finalize()缺点:运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不建议覆盖。

判定一个常量是否无用:没有引用就是无用~

判定一个类是否无用:1.Java堆中不存在该类的任何实例;2.加载该类的ClassLoader已经被回收;3.没有反射机制访问该类

GC算法

标记-清除算法:

标记后清除。

缺点:效率低,空间碎片太多

复制算法:

将内存等分,一次只用一边,每次内存回收时,把存货的对象复制到另一块,然后回收一整块。

实现简单,运行高效,没有碎片问题

缺点:需要将内存缩小为原来的一般

标记-整理算法:

标记如同标记清楚算法,后续把所有存活的对象往一端移动,然后直接清理掉边界外的内存。

新生代死去的对象非常多,因此使用复制算法;老年代对象存活率高,因此使用标记算法。

垃圾收集器

HotSpot虚拟机的垃圾收集器:

Serial收集器

新生代虚拟机。

单线程的收集器,在它进行GC时,必须暂停其他所有的工作线程(所谓的Stop The World)

优点:简单而高效,由于没有线程交互的开销,因此专心做GC。。。对于运行在Client模式下的虚拟机是很好的选择

新生代采用复制算法,暂停所有用户线程。(GC线程只有一个)

ParNew收集器

新生代虚拟机。

多线程版本的Serial收集器。

因为目前只有Serial和ParNew能和CMS收集器合作,因此它是很多Server模式的虚拟机的首选。

新生代采用复制算法,暂停所有用户线程。老年代使用标记-整理算法,暂停所有用户线程。(GC线程有多个)

Parallel Scavenge收集器

新生代收集器。

并行的多线程收集器,也是使用复制算法。

Parallel Scavenge收集器的目的是达到一个可控制的吞吐量

自适应调节策略

并发与并行

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾手机程序运行于另一个CPU中。

Serial Old收集器

Serial收集器的老年代版本。使用标记-整理算法。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。

使用多线程和标记-整理算法。

用于和Parllel Scavenge收集器配合,达到“吞吐量优先”组合。

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的老年代收集器。

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

缺点:1.对CPU资源非常敏感,可能造成用户程序执行速度降低(采用过增加GC过程的时间,但是效果不好)

2.CMS收集器无法处理浮动垃圾,由于CMS并发处理过程中用户进程还在运行,部分垃圾出现在标记结束之后,因此得等待下次GC,即所谓“浮动垃圾”。

3.由于CMS使用的是“标记-清除”算法,因此会有大量的空间碎片。

G1收集器

当今收集器技术最前沿成果之一。

特点:

  • 并行与并发:能充分利用多CPU,缩短STW停顿的时间,可以通过并发来让Java程序在GC时继续运行
  • 分代收集:G1可以不需要其他收集器配合就独立管理整个GC堆,但它能够采取不同方式来处理不同状态的对象
  • 空间整合:整体上看使用“标记-整理”算法,局部上看使用复制算法,因此不存在内存空间碎片问题
  • 可预测的停顿:除了追求低停顿外,还能建立可预测的停顿时间模型
  • 内存“化整为零“:将整个Java堆划分为多个大小相等的独立区域(Region),根据允许的收集时间优先收回价值最大的Region。通过Remembered Set技术来实现不同Region的对象引用问题

G1运作步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

理解GC日志

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

最前面的数字:GC发生的时间,从虚拟机启动以来经过的秒数.

“[GC”和“[Full GC”:Full代表这次GC发生了STW.

“[Defnew”等等:GC发生的区域,不同的收集器有不同的名称

“3324K->152K(3712K)”:GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)

“3324K->152K(11904K)”:GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)

“0.0025925sec”:该内存区域GC所占用的时间

user、sys、real:用户态、内核态、操作开始到结束所经过的墙钟时间(包括各种如磁盘IO、等待线程阻塞等时间)

垃圾收集器参数总结

内存分配与回收策略

对象优先在Eden分配(使用Serial/SerialOld收集器组合)

package com.xiaoyu.chap3.GC;

/**
* Created by xiaoyu on 16/4/6.
* VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:PrintGCDetails -XX:SurvivorRation=8 UseSerialGC
*/
public class TestAllocation { private static final int _1MB = 1024*1024; public static void testAllocation(){
byte[] allocation1,allocation2,allocation3,allocation4; allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; //出现一次Minor GC } public static void main(String[] args) {
TestAllocation.testAllocation();
}
} output:
[GC (Allocation Failure) [DefNew: 7635K->533K(9216K), 0.0070160 secs] 7635K->6677K(19456K), 0.0070590 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4931K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 53% used [0x00000007bec00000, 0x00000007bf04ba80, 0x00000007bf400000)
from space 1024K, 52% used [0x00000007bf500000, 0x00000007bf5854b8, 0x00000007bf600000)
to space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
tenured generation total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)
Metaspace used 3056K, capacity 4494K, committed 4864K, reserved 1056768K
class space used 336K, capacity 386K, committed 512K, reserved 1048576K

从上面可以看出:

①前6MB数据分配到Eden区后,Eden区所剩的内存已经不足以分配allocation4了,因此发生MinorCG

②MinorGC之后虚拟机发现已有的3个2MB大小的对象无法放入Survivor空间,因此只能通过分配担保机制提前转移到老年代。

③GC结束后,allocation4被分配在Eden区,Survivor空闲,老年代被占用6MB(allocation1、2、3)

大对象直接进入老年代

package com.xiaoyu.chap3.GC;

/**
* Created by xiaoyu on 16/4/6.
* VM args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
* -XX:PretenureSizeThreshold参数令大小大于设定值的对象直接在老年代分配
*/
public class TestPretenureSizeThreshold { private static final int _1MB = 1024*1024; public static void testPretenureSizeThreshould(){
byte[] allocation;
allocation = new byte[4*_1MB];
} public static void main(String[] args) {
testPretenureSizeThreshould();
} }
output:
Heap
def new generation total 9216K, used 1655K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 20% used [0x00000007bec00000, 0x00000007bed9dd60, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
Metaspace used 3033K, capacity 4494K, committed 4864K, reserved 1056768K
class space used 334K, capacity 386K, committed 512K, reserved 1048576K

①可以发现,allocation对象直接被分配到了老年代中。

②PretenureSizeThreshold参数只对Serial和ParNew收集器有效!

长期存活的对象将进入老年代

package com.xiaoyu.chap3.GC;

/**
* Created by xiaoyu on 16/4/6.
*
* VM args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:UseSerialGC
* -XX:MaxTenuringThreshold来设置对象晋升老年代的年龄阈值
*/ public class TestTenuringThreshold { private static final int _1MB = 1024*1024; @SuppressWarnings("unused")
public static void testYenuringThreshold(){
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[_1MB/4]; //什么时候进入老年代取决于XX:MaxTenuringThreshold设置
allocation2 = new byte[_1MB*4];
allocation3 = new byte[_1MB*4];
allocation3 = null;
allocation3 = new byte[_1MB*4];
} public static void main(String[] args) {
testYenuringThreshold();
} }

①MaxTenuringThreshold=1时,allocation1对象在第二次GC时就会进入老年代,新生代已使用的内存GC后会就变为0KB

②MaxTenuringThreshold=15时,allocation1对象在第二次GC后还会留在Survivor。

动态对象年龄判断

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代。

总结

内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。

Java学习之垃圾回收的更多相关文章

  1. java学习之 垃圾回收

    垃圾回收器始终以一个较低优先级的后台进程进行垃圾的回收工作,这样不会影响程序的正常工作. 通常只有当内存到达用尽的边缘而程序需要分配新的内存空间时,垃圾回收器才会执行. 垃圾回收的条件:1,垃圾回收器 ...

  2. Java学习之垃圾回收机制

    垃圾回收机制,依赖JRE和JVM,涉及操作系统中内存的分配与回收.依据所学,我猜想这种机制需要的数据结构是堆内存分配表(链),管理已分配和未分配的堆内存,对于已分配堆内存,需要知道由栈内存中的哪些变量 ...

  3. Java编程思想学习笔记_1(Java内存和垃圾回收)

    1.Java中对象的存储数据的地方: 共有五个不同的地方可以存储数据. 1)寄存器.最快,因为位于处理器的内部,寄存器按需求分配,不能直接控制. 2)堆栈.位于通用RAM,通过堆栈指针可以从处理器那里 ...

  4. 高吞吐低延迟Java应用的垃圾回收优化

    高吞吐低延迟Java应用的垃圾回收优化 高性能应用构成了现代网络的支柱.LinkedIn有许多内部高吞吐量服务来满足每秒数千次的用户请求.要优化用户体验,低延迟地响应这些请求非常重要. 比如说,用户经 ...

  5. 每日一问:讲讲 Java 虚拟机的垃圾回收

    昨天我们用比较精简的文字讲了 Java 虚拟机结构,没看过的可以直接从这里查看: 每日一问:你了解 Java 虚拟机结构么? 今天我们必须来看看 Java 虚拟机的垃圾回收算法是怎样的.不过在开始之前 ...

  6. JVM学习笔记——垃圾回收篇

    JVM学习笔记--垃圾回收篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分 我们会分为以下几部分进行介绍: 判断垃圾回收对象 垃圾回收算法 分代垃圾回收 垃圾回收器 ...

  7. Java虚拟机之垃圾回收详解一

    Java虚拟机之垃圾回收详解一 Java技术和JVM(Java虚拟机) 一.Java技术概述: Java是一门编程语言,是一种计算平台,是SUN公司于1995年首次发布.它是Java程序的技术基础,这 ...

  8. 【java虚拟机序列】java中的垃圾回收与内存分配策略

    在[java虚拟机系列]java虚拟机系列之JVM总述中我们已经详细讲解过java中的内存模型,了解了关于JVM中内存管理的基本知识,接下来本博客将带领大家了解java中的垃圾回收与内存分配策略. 垃 ...

  9. java中存在垃圾回收机制,但是还会有内存泄漏的问题,原因是

    答案是肯定的,但不能拿这一句回答面试官的问题.分析:JAVA是支持垃圾回收机制的,在这样的一个背景下,内存泄露又被称为“无意识的对象保持”.如果一个对象引用被无意识地保留下来,那么垃圾回收器不仅不会处 ...

随机推荐

  1. Access 是/否 字段

    Microsoft Access 数据库引擎 SQL 数据类型包含由 Microsoft® Jet 数据库引擎定义的 13 种主要数据类型,以及若干可识别为这些数据类型的有效同义词. 其中,在工作中遇 ...

  2. poj 3084(最小割)

    题目链接:http://poj.org/problem?id=3084 思路:题目的意思是不让入侵者进入保护的房间,至少需要锁几道门.网络流建模:设一个超级源点,源点与有入侵者的房间相连,边容量为in ...

  3. 简单理解Javascript中的call 和 apply

    javascript中面向对像的能力是后来加进来的, 为了兼容性, 所以整出了很多奇特的东西, function Animal(){ this.name = "Animal"; t ...

  4. 一起talk C栗子吧(第二十五回:C语言实例--二分查找)

    各位看官们,大家好,上一回中咱们说的是顺序查找的样例,这一回咱们说的样例是:二分查找.闲话休 提,言归正转. 让我们一起talk C栗子吧. 看官们,我们在上一回中说了查找的相关内容,而且介绍了一种查 ...

  5. Spring框架中的AOP技术----注解方式

    利用AOP技术注解的方式对功能进行增强 CustomerDao接口 package com.alphajuns.demo1; public interface CustomerDao { public ...

  6. C#封装C++DLL(特别是char*对应的string)

    1.新建一个C#-Windows-类库(用于创建C#类库(.dll)的项目)类型的工程 2.对于普通C++函数 XXXX_API void cppFun(int i); 在cs代码中添加 [DllIm ...

  7. 面试题思考:IO 和 NIO的区别,NIO优点

    面试时答: IO是面向流的,NIO是面向缓冲区的 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方: NIO则能前后移动流中的数据,因为是面向缓冲区的 ...

  8. Django - admin后台、auth权限

    admin后台 一.创建一个管理员用户 (1).设置时区.语言(可选步骤) 打开settings.py,改成下面那样 LANGUAGE_CODE = 'zh-Hans' TIME_ZONE = 'As ...

  9. Ubuntu 14.04下安装GitLab

    0.硬件要求 官方要求:http://doc.gitlab.com/ce/install/requirements.html CPU 1 core works supports up to 100 u ...

  10. IO流入门-第七章-BufferedReader

    BufferedReader基本用法和方法示例 /* 字节 BufferedInputStream BufferedOutputStream 字符 BufferedReader:带有缓冲区的字符输入流 ...