public class GCTest {
public static void main(String[] args) {
Student student1 = new Student();
Student student2 = new Student();
student1.student = student2 ; //这种情况GC就无法回收
student2.student = student1 ;
}
}
class Student{
int id ;
Student student ;
}
//1、t1可以叫虚拟机栈中引用的对象(由于强引用不可回收)
GCRootsDemo t1 = new GCRootsDemo();
//2、方法区中的类静态属性引用的对象
private static GCRootsDemo2 t2 = new GCRootsDemo2();
//3、方法区中常量引用的对象
private static finalGCRootsDemo3 t3 = new GCRootsDemo3(); 特点:都不会被回收的,可以作为根
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
Object o=new Object(); // 强引用
o=null; // 帮助垃圾收集器回收此对象
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
//当我们内存不够用的时候,soft会被回收的情况
SoftReference<MyObject> softReference = new SoftReference<>(new Object());
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
//垃圾回收机制一运行,会回收该对象占用的内存
WeakReference<MyObject> weakReference = new WeakReference<>(new Object());
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。【不推荐使用,过于复杂】
除强引用外,其余四种引用一旦没有被GCRoot引用,当被垃圾回收的时候,引用都会进入引用队列
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue();
//和引用队列进行关联,当虚引用对象被回收后,会进入ReferenceQueue队列中
PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);
/** * 演示 软引用 * -Xmx20m -XX:+PrintGCDetails -verbose:gc 设置堆Heap内存的大小为20M ,打印详细的GC信息 */
public class Code_08_SoftReferenceTest {
public static int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
method2();
}
// 设置 -Xmx20m , 演示堆内存不足,
public static void method1() throws IOException {
ArrayList<byte[]> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
// 演示 软引用
public static void method2() throws IOException {
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]); //软引用
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for(SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。
软引用关联一个引用队列
// 演示 软引用 搭配引用队列
public static void method3() throws IOException {
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i = 0; i < 5; i++) {
// 关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while(poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("=====================");
for(SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
public class Code_09_WeakReferenceTest {
public static void main(String[] args) {
// method1();
method2();
}
public static int _4MB = 4 * 1024 *1024;
// 演示 弱引用
public static void method1() {
List<WeakReference<byte[]>> list = new ArrayList<>();
for(int i = 0; i < 10; i++) {
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB]);
list.add(weakReference);
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
}
// 演示 弱引用搭配 引用队列
public static void method2() {
List<WeakReference<byte[]>> list = new ArrayList<>();
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i = 0; i < 9; i++) {
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
list.add(weakReference);
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
System.out.println("===========================================");
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
}
第一步:标记出养老区的所有垃圾,第二步:清除所有的垃圾
在标记清除的步骤后,在将老年代通过整理算法整理一下空间,解决内存碎片问题
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
大对象处理策略:
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代。
线程内存溢出:
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行。
这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。
正式进入前先看下图解HotSpot虚拟机所包含的收集器:
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
几个相关概念
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一
个CPU上
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时
间)),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%。
下面来了解一下垃圾回收器的分类:
串行垃圾回收器开启语句:-XX:+UseSerialGC = Serial + SerialOld
Serial
:表示新生代,采用复制算法;SerialOld
:表示老年代,采用的是标记整理算法。
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。
Serial(新生代)收集器是最基本的、发展历史最悠久的收集器:
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交
互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束
(Stop The World)
注意:
ParNew(新生代) 收集器其实就是 Serial 收集器的多线程版本
特点:多线程、采用复制算法**、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参
数来限制垃圾收集的线程数。除了使用多线程外其余行为均和Serial收集器一模一样存在 Stop The World 问题,可以和CMS收集器连用
Serial Old 是 Serial 收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
// 1.吞吐量优先垃圾回收器开关:(默认开启)
-XX:+UseParallelGC~-XX:+UseParallelOldGC
// 2.采用自适应的大小调整策略:调整新生代(伊甸园 + 幸存区FROM、TO)内存的大小
-XX:+UseAdaptiveSizePolicy
// 3.调整吞吐量的目标:吞吐量 = 垃圾回收时间/程序运行总时间 1/(1+ratio) 当ratio取19时,那么吞吐量就是1/20
-XX:GCTimeRatio=ratio
// 4.垃圾收集最大停顿毫秒数:默认值是200ms
-XX:MaxGCPaiseMillis=ms
// 5.控制ParallelGC运行时的线程数
-XX:ParallelGCThreads=n
与吞吐量关系密切,故也称为吞吐量优先收集器:(用尽可能少的时间回收垃圾)
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)。
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy
参数。当开关打开时不需要手动指定新生代的大小
(-Xmn)
、Eden与Survivor区的比例(-XX:SurvivorRation)
、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)
等,虚
拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适
应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
是Parallel Scavenge收集器的老年代版本:
特点:多线程,采用标记-整理算法(老年代没有幸存区)。
// 开关:
-XX:+UseConMarkSweepGC~-XX:+UseParNewGC~SerialOld
// ParallelGCThreads=n并发线程数
// ConcGCThreads=threads并行线程数 ,垃圾回收线程一般是并发执行的总线程数的1/4,也就是1个垃圾回收线程和3个用户线程争抢cpu
-XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads
// 执行CMS垃圾回收的内存占比:预留一些空间保存浮动垃圾:因为并发清理时候会产生浮动垃圾,这些浮动垃圾没地方存
-XX:CMSInitiatingOccupancyFraction=percent
// 重新标记之前,对新生代进行垃圾回收,这样会减轻我们重新标记的压力
-XX:+CMSScavengeBeforeRemark
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器:
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
CMS收集器的内存回收过程是与用户线程一起并发执行的
参考文章:垃圾收集器G1详解
适用场景:
相关参数:JDK8 并不是默认开启的,需要参数开启:
// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:,默认分别为2048个去区域
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间(延时时间),默认是250ms
-XX:MaxGCPauseMillis=time
设计的目的是按照Region回收,优先回收价值最大的(回收后所能得到最大的空间),这样每次回收一小块降低等待时间!
为了解决上述的跨代引用问题,我们引入CardTable (卡表)和 RememberSet
CardTable:(卡表)
RemeberSet :(记忆集合)
当我们需要对Region2进行回收的时候,通过Rset可以直接定位到Region的哪个card,无需扫描全部!
会Stop The World (STW)
新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)。
分区算法Region
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,
方便控制 GC 产生的停顿时间。虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需
要连续)的动态集合 Collection Set。E:伊甸园 S:幸存区 O:老年代
1、新创建对象放入E:伊甸园区,
2、当我们的Eden区满了以后执行垃圾回收时,会把伊甸园(E)的幸存对象复制到幸存区(S):
3、当幸存区(s)中的对象也比较多触发垃圾回收,且幸存对象寿命超过阈值时,幸存区(S)中的一部分对象(寿命达到阈值)会晋升到老年代(O),寿命未达到阈值的会被再次复制到另一个幸存区(S):
CM(Concurrent Mark):并发标记!
-XX:InitiatingHeapOccupancyPercent=percent // 默认值45%
会对E、S 、O 进行全面的回收。
// 用于指定GC最长的停顿时间
-XX:MaxGCPauseMillis=ms
MixGC的回收过程可以理解为YoungGC后附加的全局concurrent marking,全局的并发标记主要用来处理old区(包含H区)的存活对象
标记,过程如下:1. 初始标记(InitingMark) 2. 根分区扫描(RootRegionScan)3. 并发标记(ConcurrentMark)4. 最终标记
(Remark)5. 清除阶段(Clean UP)
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的最大停顿时间,会根据最大停顿时
间,有选择的回收最有价值的老年代(回收后,能够得到更多内存)。
G1在老年代内存不足时(老年代所占内存超过阈值):
SerialGC
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc
ParallelGC
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc
CMS
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集,需要分2种情况,这里不做详细介绍
G1
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集,需要分2种情况,这里不做详细介绍
G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发FullGC。FullGC使用的是stop the
world的单线程的Serial Old模式,所以一旦触发FullGC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full gc的处理的。
对于G1 GC的优化,很大的目标就是没有FullGC。
post-write barried + dirty card queue
。重新标记阶段
在垃圾回收时,收集器处理对象的过程中:
黑色:已被处理,需要保留的
灰色:正在处理中的
白色:还未处理的
但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到
remark。
优点与缺点:
字符串去重开启指令 -XX:+UseStringDeduplication:
案例分析:
String s1 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
String s2 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
将所有新分配的字符串(底层是char[])放入一个队列。
当新生代回收时,G1并发检查是否有重复的字符串。
如果字符串的值一样,就让他们引用同一个字符串对象。
注意,其与String.intern()的区别:
intern关注的是字符串对象。
字符串去重关注的是char[]数组。
在JVM内部,使用了不同的字符串标。
在所有对象经过并发标记阶段以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用时,则卸载它所加载的所有类。
并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark
默认启用。
巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉~
YoungGC阶段:就是跟我们普通的GC一样,针对新生代,Eden满了然后通过复制算法道Survivor,S满了到Old的过程
MixedGC阶段:类CMS类似,但是他是针对新生代和老年代的全局,按照如下流程:
1、初始标记,标记出所有的GCRoot
2、并发标记,tracing GCroot(对GCroot进行跟踪),找出所有的活跃节点,但是此时是与用户线程并发执行的,所以不准确
3、最终标记(重新标记remark):此时会STW,以及对新对象和队列中的对象(参考remark)进行重新判断,确认
4、复制/清除阶段,这个阶段会优先对可回收空间较大的Region进行回收,即garbage first,这也是G1名称的由来。
这四个步骤
FullGC阶段:G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发FullGC。FullGC使用的是stop the world的单线程的Serial Old模式,所以一旦触发FullGC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full gc的处理的。对于G1 GC的优化,很大的目标就是没有FullGC。
未来:ZGC 与 Shenandoah
查看虚拟机参数命令
D:\JavaJDK1.8\bin\java -XX:+PrintFlagsFinal -version | findstr "GC"
可以根据参数去查询具体的信息
低延迟/高吞吐量? 选择合适的GC
首先排除减少因为自身编写的代码而引发的内存问题
查看 Full GC 前后的内存占用,考虑以下几个问题
数据是不是太多?
resultSet = statement.executeQuery(“select * from 大表 limit n”)
数据表示是否太臃肿 ?
对象图
对象大小 16 Integer 24 int 4
是否存在内存泄漏 ?
static Map map …
软
弱
第三方缓存实现
所有的 new 操作分配内存都是非常廉价的
新生代对每个线程都有对应的本地缓存池 :TLAB thread-lcoal allocation buffer
死亡对象回收零代价
大部分对象用过即死(朝生夕死)
Minor GC 所用时间远小于 Full GC
不是
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution
以 CMS 为例:
-XX:CMSInitiatingOccupancyFraction=percent
案例1:Full GC 和 Minor GC 频繁
案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
虚拟机参数命令
D:\JavaJDK1.8\bin\java -XX:+PrintFlagsFinal -version | findstr "GC"
可以根据参数去查询具体的信息
低延迟/高吞吐量? 选择合适的GC
首先排除减少因为自身编写的代码而引发的内存问题
查看 Full GC 前后的内存占用,考虑以下几个问题
数据是不是太多?
resultSet = statement.executeQuery(“select * from 大表 limit n”)
数据表示是否太臃肿 ?
对象图
对象大小 16 Integer 24 int 4
是否存在内存泄漏 ?
static Map map …
软
弱
第三方缓存实现
所有的 new 操作分配内存都是非常廉价的
新生代对每个线程都有对应的本地缓存池 :TLAB thread-lcoal allocation buffer
死亡对象回收零代价
大部分对象用过即死(朝生夕死)
Minor GC 所用时间远小于 Full GC
不是
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution
以 CMS 为例:
-XX:CMSInitiatingOccupancyFraction=percent
案例1:Full GC 和 Minor GC 频繁
案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
案例3:老年代充裕情况下,发生 Full GC(jdk1.7)
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_46571920/article/details/121579597
内容来源于网络,如有侵权,请联系作者删除!