文章目录:
1.什么是垃圾回收?
GC的历史(了解):
2.怎么自定义垃圾?
1、引用计数算法(Python)
2、可达性分析算法(根搜索算法,追踪性垃圾收集)
3.怎么回收垃圾?
1.标记清除算法:编辑
2.复制回收算法:
3.标记整理算法 :
4.分代收集算法:
堆内存模型 与 回收策略(分代收集):
1.Eden 区:
2.Survivor 区:
3.Old 区:
4.为什么需要两个Survivor区?
针对以下几种情况也会进入老年代:
大对象:
长期存活对象:
内存碎片:
回收器选择:
1、Serial收集器(单线程)
2、ParNew收集器(多线程)
3.Parallel Scavenge(多线程)8
4.Serial Old(单线程)
5.Parallel Old(多线程)8
6、CMS收集器(多线程)
7、G1收集器
1.什么是垃圾回收?
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止
内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用
的对象进行清除和回收
2.怎么自定义垃圾?
既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的?
定义垃圾要先了解:
1、引用计数算法(Python)
2、可达性分析算法(根搜索算法,追踪性垃圾收集)
1、引用计数算法(Python)
引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该 对象被引用的次数(Reference Count)。如果该对象被引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0 时,那么该对象就会被回收。
String m = new String("jack");
先创建一个字符串,这时候"jack"有一个引用,就是 m。
然后将 m 设置为 null,这时候"jack"的引用次数就等于0了,在引用计数算法中,意 味着这块内容就需要被回收了。
m = null;
(这个方法的弊端):看似很美好,最终 还是需要放弃 引用计数算法 ,看下面的例子。
public class User{
private String name;
public Object obj;
public User(String name){}
}
public static void testGC(){
User a = new User("objA");
User b = new User("objB");
a.obj= b;
b.obj= a;
a = null;
b = null;
}
1. 定义2个对象
2. 相互引用
3. 置空各自的声明引用
总结:我们看图可以看到,最后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。(循环引用的问题)
1.4 怎么回收垃圾?
引用计数法:在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
可达性分析
如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
可作为GC Roots的对象:
虚拟栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
JVM内部的引用
所有被同步锁(Synchronized关键字)持有的对象
反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
1.4 怎么回收垃圾?
分代收集理论——经验法则
弱分代假说:绝大多数对象都是朝生夕灭的
强分代假说:熬过越多次垃圾收集过程的对象越难消亡
基于以上两点,收集器应该将Java堆划分出不同的区域,然后将回收对象依据年龄等分配到不同的区域中存储。但是可能会有跨代引用,于是就有了
跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
只需在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分为若干小块,标识出老年代的哪一块内存会存在跨代引用。当发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
标记-清除——效率不稳定、空间碎片化。最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
标记-复制——为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。新生代MinorGC使用这个。
标记-整理——结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。老年代FullGC使用这个。
4.分代收集算法:
严格来说并不是一种具体算法,而是融合上述3种基础的算法思想,而产生的针对不
同情况所采用不同算法的一套组合拳。 对象存活周期的不同将内存划分为几块。 一般
是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收
集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就
选用复制算法,因为只需要付出少量存活对象的复制成本就可以完成收集。而老年代
中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标
记整理算法来进行回收。
堆内存模型 与 回收策略(分代收集):
Java 堆(Java Heap)是JVM所管理的内存中最大的一块,堆又是垃圾收集器管理的 主要区域,这里我们主要分析一下 Java 堆的结构: Java 堆主要分为2个区域-新生代与老年代(java1.8之后),其中新生代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。
Minor GC 、 Major GC 、 Full GC ?
新生代内存不够用时候发生Minor GC 也叫 Yong GC ,老年代内存不够的时候发生 Major GC, Minor GC 相比 Major GC 更频繁,回收速度也更快。 还有一种GC 负责整个新生代 + 老年代的回收称为 Full GC.
1.Eden 区:
业务处理中,绝大多数对象是朝生夕死,这种对象会在新生代 Eden 区中进行内存分 配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC。
通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些 无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
2.Survivor 区:
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。 Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会 将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进 入 Old 区)。
3.Old 区:
老年代占据着2/3的堆内存空间,只有在Major GC(full gc) 的时候才会进行清理, 每次 GC 都会触发“ Stop-The-World ”。内存越大,STW 的时间也越长,所以内存 也不仅仅是越大就越好由于复制算法在对象存活率较高的老年代会进行很多次的复 制操作,效率很低,所以老年代这里采用的是 标记-整理 算法。
Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的 其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许 GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运 行。
4.为什么需要两个Survivor区?
设置两个 Survivor 区最大的好处就是解决内存碎片化,可以累计对象的年龄迟一点
进入老年代
我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区 被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有 一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只 能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消 亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区 域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区 域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中 的存活对象再复制到 From 区域,以此反复。
在from和to之间经历15次 Minor GC 之后,就会被送到老年代。
这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一 个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方 说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比 较小,容易导致 Survivor 区满,两块 Survivor 区是经过权衡之后的最佳方案。
针对以下几种情况也会进入老年代:
大对象:
大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进 到老年代。这样做主要是为了避免在 Eden 区及2个 Survivor 区之间发生大量的内存复制。
当你的系统有非常多“朝生夕死”的大对象时,得注意了。
长期存活对象:
虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年 龄就增加1岁。当年龄增加到15岁时,这时候就会被转移到老年代。当然,这里的15,JVM 也支持进行特殊设置。
内存碎片:
一旦实例从堆内存中被删除,其位置就会变空并且可用于未来实例的分配。这些 空出的空间将会使整个内存区域碎片化。为了实例的快速分配,需要进行碎片整理。基于垃 圾回收器的不同选择,回收的内存区域要么被不停地被整理,要么在一个单独的GC进程中 完成。
回收器选择:
JVM 给了三种选择: 串行收集器、并行收集器、并发收集器 ,但是串行收集器只适用于小 数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下, JDK1.5 以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。 JDK1.5 以 后, JVM 会根据当前系统配置进行判断。
1.吞吐量优先的并行收集器 :
并行收集器主要以到达一定的吞吐量为目标。
2.响应时间优先的并发收集器:
并发收集器主要是保证系统的响应时间,减少垃圾收集时的停 顿时间。
我整理出来了7种收集器:
1、Serial收集器(单线程)
是新生代,一个单线程的收集器,采用的是复制算法;
现在依然是虚拟机运行在Client模式下的默认新生代收集器,主要就是因为它简单而高效
2、ParNew收集器(多线程)
其实就是Serial收集器的多线程版本,采用的也是复制算法,他也是新生代;
ParNew收集器在单CPU环境中绝对不会有比Serial收集器更好的效果;
是许多运行在Server模式下虚拟机首选的新生代收集器,重要原因就是除了Serial收集器 外,只有它能与CMS收集器配合工作;
3.Parallel Scavenge(多线程)8
收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线
程,其采用的是复制算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控 的吞吐量。
4.Serial Old(单线程)
收集器是针对老年代的收集器,采用的是标记整理算法。它的优点是实现简单高效,但是缺
点是会给用户带来停顿。
5.Parallel Old(多线程)8
Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和标记整理算法。
6、CMS收集器(多线程)
是一个老年代收集器,基于标记清除算法,设计目标是尽量减少停顿时间,存在着内存碎片化问
题,所以难以避免长时间运行情况下发生full GC,导致恶劣的停顿另外,既然强调了并发, CMS 会占用更多 CPU 资源,并和用户线程争抢。
7、G1收集器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多台处理器及大量内存的
机器以及高概率满足gc定顿时间同时,还具备吞吐量高性能特征:
查看jdk默认垃圾收集器 java -XX:+PrintCommandLineFlags -version
查看堆内存中各个区的使用率 java -XX:+PrintGCDetails -version
-
总结:Java垃圾收集(GC)是一种内存管理机制,自动回收不再使用的对象占用的内存空间。选择合适的垃圾收集器能够优化应用程序的性能。监控和分析垃圾收集可以帮助识别和解决性能问题。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/LJWfbj666/article/details/139120947