java垃圾回收的那点事

垃圾回收实现

引用计数算法

给对象添加一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器减1。引用计数为0的对象可被回收。

两个对象出现循环引用的情况下,此时引用计数器永远不为0,导致无法对它们进行回收。

因为循环引用的存在,所以 Java 虚拟机不适用引用计数算法。

可达性分析算法

通过一系列的称为GC Roots的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象不可用。即能到达的对象视为存活,不能到达的对象视为失活。

可作为GC Roots的对象包括以下几种:

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

引用类型

无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。

强引用

被强引用关联的对象不会被垃圾收集器回收。

创建方法:使用new一个新对象的方式来创建强引用。

Object obj = new Object();

软引用(Soft Reference)

被软引用关联的对象,只有在内存不够的情况下才会被回收。

创建方法:使用 SoftReference 类来创建软引用。

Object obj = new Object();

SoftReference<Object> sf = new SoftReference<Object>(obj);

obj = null; // 使对象只被软引用关联

弱引用(Weak Reference)

被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。

创建方法:使用 WeakReference 类来实现弱引用。

Object obj = new Object();

WeakReference<Object> wf = new WeakReference<Object>(obj);

obj = null;

 WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>

虚引用

又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

创建方法:使用 PhantomReference 来实现虚引用。

Object obj = new Object();

PhantomReference<Object> pf = new PhantomReference<Object>(obj);

obj = null;

pf.get();//永远返回null

pf.isEnQueued();//返回从内存中已经删除

虚引⽤必须和引⽤队列(ReferenceQueue)联合使⽤。当垃圾回收器准备回收⼀个对象时,如果发现它还有虚引⽤,就会在回收对象的内存之前,把这个虚引⽤加⼊到与之关联的引⽤队列中。程序可以通过判断引⽤队列中是 否已经加⼊了虚引⽤,来了解被引⽤的对象是否将要被垃圾回收。程序如果发现某个虚引⽤已经被加⼊到引⽤队列,那么就可以在所引⽤的对象的内存被回收之前采取必要的⾏动。

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比年轻代差很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

jvm内存模型

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,是当前线程所执行的字节码的行号指示器。

字节码解释器在解释执行字节码文件工作时,每当需要执行一条字节码指令时,就通过改变程序计数器的值来完成。程序中的分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
程序执行过程中,会不断的切换当前执行线程,切换后,为了能让当前线程恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,并且各线程之间计数器互不影响,独立存储。

计数器的作用

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候,能够知道当前线程的运行位置。
程序计数器是唯一一个不会出现 OutOfMemoryError的内存区域,它随着线程的创建而创建,随着线程的结束而死亡。

虚拟机栈

与程序计数器一样,VM Stack虚拟机栈也是线程私有的,它的生命周期和线程相同,用于描述 Java 方法执行时的内存模型,每次方法调用的数据都是通过栈传递的。

JMM内存区域可以粗略的区分为堆内存(Heap)和栈内存 (Stack)。其中栈就是VM Stack虚拟机栈,或者说是虚拟机栈中局部变量表部分。

局部变量表主要存放了编译期可知的各种基本数据类型变量值(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java 方法有两种返回方式,不管哪种返回方式都会导致当前活动栈帧被弹出

  • return 语句
  • 抛出异常

Java 虚拟机栈会出现两种错误

StackOverFlowError

当线程请求栈的深度超过 JVM虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

OutOfMemoryError

JVM的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异。

本地方法栈

本地方法栈用于虚拟机调用的 Native方法。

native关键字修饰的本地方法被执行的时候,在本地方法栈中也会创建一个栈帧,用于存放该native本地方法的局部变量表、操作数栈、动态链接、方法出口信息。方法执行完毕后,相应的栈帧也会出栈并释放内存空间。也会出现 StackOverFlowError和 OutOfMemoryError两种错误。

方法区(元空间)线程共享

方法区是在jdk1.6是有的,jdk1.8没有方法区了,新增元空间,元空间中有运行时常量池。

作用

方法区存储的是:常量池、静态变量(static)以及方法信息(修饰符、方法名、返回值、参数等)、类信息(类变量)等。

jdk发展

1.JDK 1.6

HotSpot JVM 使用Method Area方法区存储,也叫永久代(Permanent Generation)。

方法区和“永久代(Permanent Generation)”的区别:方法区是JVM 的规范,而永久代(Permanent Generation)是 JVM规范的一种实现,并且只有 HotSpot JVM才有永久代“Permanent Generation”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有;
方法区是一片连续的堆空间,当JVM加载的类信息容量超过了最大可分配空间,虚拟机会抛出OutOfMemoryError:PermGenspace的Error。
永久代的GC是和老年代(old generation)捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。
可以通过 -XX:PermSize=N 设置 方法区 (永久代) 初始空间,-XX:MaxPermSize=N 设置方法区 (永久代) 最大空间,超过这个值将会抛出错误:java.lang.OutOfMemoryError: PermGen

2.JDK 1.7

将字符串常量池、静态变量转移到了堆区。

3.JDK 1.8

正式移除永久代,采用 Meta Space 元空间替代。

元空间的本质和永久代类似,都是对JVM规范中方法区的一种具体实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过运行参数来指定元空间的大小。

堆(Heap)线程共享

概念

堆是线程共享的内存区域,它是虚拟机管理内存中最大的一块。

堆储存的是:实例对象。

比如 A a1 = new A();a1就是实例对象。A a2;a2就是类对象。

YGC(Minor gc)

触发条件

新生代内存空间不足触发。

回收步骤

1.创建一个新对象,在堆中分配内存

2.大部分情况下,对象会在 Eden 区生成,当 Eden 区装满时,会触发 Young Garbage Collection,即 YGC 垃圾回收时,在 Eden 区实现清除策略,没有被引用的对象直接被回收
依然存活的对象会被移送到 Survivor 区。
3.Survivor 区分为 s0 和 s1 两块内存区域,每次 YGC(Minor gc) 的时候,将存活的对象复制到未使用的 Survivor 空间(s0 或 s1),然后清空正在使用的空间,交换 s0 和 s1 的使用状态,每次交换时, 对象的Age+1。
4.如果 YGC 要移送的对象大于 Survivor 区容量的上限,则直接移交给老年代
一个对象也不可能永远呆在新生代,在 JVM 中 一个对象从新生代晋升到老年代的阈值默认值是 15,可以在 Survivor区交换 14 次之后,晋升至老年代。

如果是新对象需要的内存空间很大,Eden区放不下,那么他就会直接创建在年老代中。

特点

回收速度快,回收时候频率比较高。

Old GC(Major GC)

触发条件

年老代内存空间不足触发。

特点

回收速度相对于YGC(Minor gc)来说,比较慢,在执行回收时候,应用的所有线程会停下来等待Old GC(Major GC)执行结束才能继续执行。

Full GC

触发条件

当整个Java堆内存空间不足时,触发Full GC。也有时候,某些情况下,Minor GC和Major GC都无法回收足够的内存时,会触发Full GC。同意和Old GC(Major GC)一样会停下应用下所有线程进行等到回收。

特点

最耗时的垃圾回收过程,因为它需要扫描整个堆内存。

垃圾回收算法

标记-清除算法(Mark Sweep)

将需要回收的对象进行标记,然后清理掉被标记的对象。

缺点

  • 标记和清除过程效率都不高;
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

复制算法(Copying)

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

缺点

  • 主要不足是只使用了内存的一半;

标记-整理算法(Mark-Compact)

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

缺点

  • 这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销;

分代收集算法(Generational Collection)

它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将 Java 堆分为年轻代和老年代。

  • 年轻代使用:复制 算法
  • 老年代使用:标记 - 清理 或者 标记 - 整理 算法

分区算法

其主要就是将整个内存分为N个多小的独立空间,每个小空间都可以独立使用,这样细粒度的控制一次回收都少个小空间和那些个小空间,而不是对整个空间进行GC,从而提升性能,并减少GC的停顿时间。

GC算法优劣标准

评价一个垃圾收集GC算法的两个标准

1.吞吐量(throughput)越高算法越好
2.暂停时间(pause times)越短算法越好

吞吐量

JVM在专门的线程[GC Threads]中执行GC 只要GC线程是活动的 就会和应用程序线程[Application Threads]争用当前可用CPU的时钟周期而吞吐量就是指应用程序线程占程序总用时的比例。

停顿

一个时间段内应用程序线程让GC线程执行而完全暂停。

垃圾回收器的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程,只有这样系统才不会有新的垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性,也有益于更好地标记垃圾对象,因此垃圾回收时,都会产生应用程序的停顿。

垃圾回收器

串行收集器

串行收集器是client 模式下的默认收集器配置。因为在客户端模式下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的年轻代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。

串行收集器采用单线程 stop-the-world 的方式进行收集。当内存不足时,串行 GC 设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行 GC 开始工作,采用单线程方式回收空间并整理内存。

单线程意味着复杂度更低、占用内存更少,垃圾回收效率高;但同时也意味着不能有效利用多核优势。事实上,串行收集器特别适合堆内存不高、单核甚至双核 CPU 的场合。

Serial收集器

开启选项:-XX:+UseSerialGC
打开此开关后,使用 Serial + Serial Old 收集器组合来进行内存回收。

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

Serial Old收集器

1、Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
2、主要意义也是在于给Client模式下的虚拟机使用。
3、如果在Server模式下,那么它主要还有两大用途:
一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],
另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

并行收集器

其他收集器都是以关注停顿时间为目标,而并行收集器是以关注吞吐量(Throughput)为目标的垃圾收集器。

  • 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验;
  • 而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

ParNew收集器

1、Serial收集器的多线程版本
2、单CPU不如Serial,因为存在线程交互的开销

-XX:+UseParNewGC 新生代并行(ParNew),老年代串行(Serial Old)

-XX:ParallelGCThreads=n 设置并行收集器收集时使用的CPU数。并行收集线程数。一般最好和计算机的CPU相当

Parallel Scavenge收集器

开启选项:-XX:+UseParallelGC
打开此开关后,使用 Parallel Scavenge + Serial Old 收集器组合来进行内存回收。
开启选项:-XX:+UseParallelOldGC
打开此开关后,使用 Parallel Scavenge + Parallel Old 收集器组合来进行内存回收。

1、吞吐量优先”收集器
2、新生代收集器,复制算法,并行的多线程收集器
3、目标是达到一个可控制的吞吐量(Throughput)。
4、吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

5、两个参数用于精确控制吞吐量:

-XX:MaxGCPauseMillis 是控制最大垃圾收集停顿时间

-XX:GCTimeRatio 直接设置吞吐量大小

-XX:+UseAdaptiveSizePolicy 动态设置新生代大小、Eden与Survivor区的比例、晋升老年代对象年龄

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

Parallel Old收集器

-XX:+UseParallelOldGC 新生代和老年代都使用并行回收收集器

1、Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
2、在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

CMS收集器

-XX:+UseConcMarkSweepGC CMS收集器通过命令行选项启用

1、以获取最短回收停顿时间为目标的收集器。
2、非常符合互联网站或者B/S系统的服务端上,重视服务的响应速度,希望系统停顿时间最短的应用
3、基于“标记—清除”算法实现的
4、CMS收集器的内存回收过程是与用户线程一起并发执行的
5、它的运作过程分为4个步骤,包括:

初始标记,“Stop The World”,只是标记一下GC Roots能直接关联到的对象,速度很快
并发标记,并发标记阶段就是进行GC RootsTracing的过程
重新标记,Stop The World”,是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,但远比并发标记的时间短
并发清除(CMS concurrent sweep)
6、优点:并发收集、低停顿
7、缺点:对CPU资源非常敏感。
无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
一款基于“标记—清除”算法实现的收集器

-XX:+UseConcMarkSweepGC 应用CMS收集器

-XX:ConcGCThreads 设置并发线程数量

-XX:CMSInitiatingOccupancyFraction 设置当老年代空间实用率达到百分比值时进行一次cms回收,默认为68,当老年代的空间使用率达到68%的时候,会执行CMS回收

如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器进行垃圾回收,这回导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以该值需要根据实际情况设置。

-XX:+UseCMSCompactAtFullCollection 设置cms在垃圾收集完成后进行一次内存碎片整理

-XX:CMSFullGCsBeforeCompaction 设定进行多少次cms回收后,进行一次内存压缩。

 

G1(Garbage-First)收集器

开启选项:-XX:+UseG1GC

1、当今收集器技术发展的最前沿成果之一
2、G1是一款面向服务端应用的垃圾收集器。
3、优点:

并行与并发:充分利用多CPU、多核环境下的硬件优势
分代收集:不需要其他收集器配合就能独立管理整个GC堆
空间整合:“标记—整理”算法实现的收集器,局部上基于“复制”算法不会产生内存空间碎片
可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
4、G1收集器的运作大致可划分为以下几个步骤:初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短
并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

 -XX:+UserG1Gc 应用G1收集器

-XX:MaxGCPauseMillis 指定最大停顿时间

-XX:ParallelGCThreads 设置并行回收的线程数量

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值