如果两个对象互相引用,计数器都为1,即使他们都没有被使用,都不会被清理。
查看哪些对象可以作为GC Root?
强引用就是沿着GC Root的引用链能够找到他他就不会被垃圾回收,只有GC Root 对他的引用都断开的时候他才能被回收,我们平时使用的引用都是强引用(Object a = new Object() a为引用)
软弱引用跟强引用的区别就是,只要A2,A3这两个对象没有被直接的强引用所引用,当垃圾回收发生时他们可能被回收掉(只有软引用引用对象时,当垃圾回收时,并且回收完还发现内存不够时,才会回收,而弱引用则在垃圾回收时直接被回收不管内存够不够)
虚引用(以Cleaner为例)
ByteBuffer分配一块直接内存,并且把直接内存地址传给虚引用对象,将来ByteBuffer一旦没有强引用所引用,ByteBuffer自己被回收掉,然后虚引用进入引用队列(队列中有 Reference Handler 线程定时到引用队列里看有没有新入队的 Cleaner 如果有该线程就会调用 Cleaner 中的 clean() 方法,clean() 方法就会根据前面记录的直接内存的地址调用 Unsafe.freeMemory() 方法去释放直接内存)
终结器finalize
1.强引用
2.软引用(SoftReference)
3.弱引用(WeakReference)
4.虚引用(PhantomReference)
5.终结器引用(FinalReference) 不用了
在接收网络上的图片资源(不属于核心业务资源)到链表中的时候如果图片数量过多,使用强引用则会导致内存溢出错误,不太重要的资源,要在内存紧张时释放掉,以后需要再读取。
不重要的资源用软引用引用
public class GCTest1 {
public static final int _4MB = 4 * 1024 * 1024;
// -Xmx20m -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
//list -X->byte[]
//list --> SoftReference -->byte[]
List<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());
}
}
}
结果:
[B@1b6d3586
1
[B@4554617c
2
[B@74a14482
3
//第3次循环内存已经很紧张了,触发了一次垃圾回收如下。
[GC (Allocation Failure) [PSYoungGen: 1875K->488K(6144K)] 14163K->13028K(19968K), 0.0007523 secs] [Times: user=0.08 sys=0.00, real=0.00 secs]
[B@1540e19d
4
/*第4次循环发现内存还是不够,又触发了minor回收如下。*/
[GC (Allocation Failure) --[PSYoungGen: 4809K->4809K(6144K)] 17349K->17349K(19968K), 0.0004952 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
/*minor GC 回收效率太低,触发了Full GC*/
[Full GC (Ergonomics) [PSYoungGen: 4809K->4499K(6144K)] [ParOldGen: 12540K->12510K(13824K)] 17349K->17009K(19968K), [Metaspace: 3219K->3219K(1056768K)], 0.0035946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
/*垃圾回收后内存还是不够,则将软连接连接的对象清除*/
[GC (Allocation Failure) --[PSYoungGen: 4499K->4499K(6144K)] 17009K->17025K(19968K), 0.0005694 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
/*minor GC 回收效率太低,触发了Full GC*/
[Full GC (Allocation Failure) [PSYoungGen: 4499K->0K(6144K)] [ParOldGen: 12526K->607K(8704K)] 17025K->607K(14848K), [Metaspace: 3219K->3219K(1056768K)], 0.0044119 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@677327b6
5
循环结束:5
null
null
null
null/前四次的对象被垃圾回收释放掉了
[B@677327b6
/*后面省略*/
由上个例子看出list集合中,在内存不足时被软引用引用的对象被释放了,但是最后遍历list的时候其中一些的软引用的对象已经是null了,对这些软引用对象已经没必要将他保留在list集合中,希望吧软引用本身也清理掉。
实现清理无用软引用:使用引用队列(ReferenceQueue泛型与软引用一致)实现对软引用的清理,当软引用关联的对象被回收时,软引用自身会被加入引用队列中去。
public class GCTest1 {
public static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
//引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
//在调用软引用构造方法时,多加一个参数(引用队列)来关联引用队列和软引用对象
//当软引用关联的 byte[] 被回收时,软引用自身会被加入引用队列
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){
//当引用队列不为null的时候
list.remove(poll);//移除list中软引用对象
poll = queue.poll();
}
System.out.println("=============打印List中的内容=============");
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
结果:
[B@1b6d3586
1
[B@4554617c
2
[B@74a14482
3
[B@1540e19d
4
[B@677327b6
5
=============打印List中的内容=============
[B@677327b6
public class GCTest1 {
public static final int _4MB = 4 * 1024 * 1024;
// -Xmx20m -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
//list -X->byte[]
//list --> WeakReference -->byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
System.out.print(ref.get()+",");
list.add(ref);
System.out.println(list.size());
}
System.out.println("=============打印List中的内容=============");
for (WeakReference<byte[]> ref : list) {
System.out.print(ref.get() + ",");
}
}
}
结果:
[B@1b6d3586,1
[B@4554617c,2
[B@74a14482,3
[B@1540e19d,4
[B@677327b6,5
=============打印List中的内容=============
[B@1b6d3586,[B@4554617c,[B@74a14482,null,[B@677327b6,
/*有时打印如下:null,null,null,null,[B@677327b6,*/
负责回收弱引用的线程优先级比较低,不触发Full GC有可能不会全部回收?
最基础的收集算法是“标记-清除”算法,如同它的名字一样,算法分 为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实在前一节讲述对象标记判定时已经介绍过了。之所以 说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而 得到的。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
为了解决效率问题,一种称为“复制”的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块 Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden 和Survivor的大小比例是8 : 1,也就是每次新生代中可用内存空间为整个新生代容量的90% (80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据;我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时, 需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
内存的分配担保就好比我们去银行借翻如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一现Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中 所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点(存活较多),有人提出予另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
当前商业虚拟机的垃圾收集都采用"分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间 对它进行分配担保,就必须使用“标记一清理”或者“标记一整理”算法来进行回收。
1.对象首先分配在Eden区内;
2.在新生代空间不足时,触发Minor GC,将Eden和From中存活的对象复制到To中,存活的对象年龄加1,并且交换To和From;
3.当老年代空间不足,尝试Minor GC,空间仍不足,触发Full GC,触发 STW,时间相比于Minor GC会长的多,因为新生代和老年代回收算法不同,且老年代中对象多,回收较为复杂。
当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或-XX:MaxHeapSize=size |
新生代大小 | -Xmn 或(-XX:NewSize=size + -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和-XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenmingThreshold=threshold |
晋升详情 | -XX:+PrintTenurmgDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
固定分配(不进行动态分配) | -XX:+UseSerialGC |
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于JDK 1.7 Update 14之 后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,之前G1仍处于实验状态)
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它 们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
虽然我们是在对各个收集 器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。这点不需要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,那HotSpot虚拟机就没必要实现那么多不同的收集器了。
开启Serial收集器 -XX:+UseSerialGC = Serial + SerialOld
这个收集器是一个单线程的收集器,但它的 “单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作, 更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生, 这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个 很好的选择。
Serial Old 收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记一 整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server 模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用。
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码 的时间与CPU总消耗时间的比值.
吞吐量=运行用户代码时间/ (运行用户代码时间+ 垃圾收集时间),虚拟机总共运行了 100分钟,其中垃圾收集花掉1分钟,那吞吐量就是 99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互
的任务。
Parallel Old是Pamllel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了 Parallel Scavenge收集器,老年代除了 Serial Old (PS MarkSweep)收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的 "拖累”,使用了 Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果, 由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬 件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC 开启Parallel收集器(在JDK 1.8 下是默认开启的,开启其中一个另一个会自动开启)
-XX:ParallelGCThreads=n 修改垃圾回收线程数量(默认和cpu核数相同)
-XX:+UseAdaptiveSizePolicy 采用自适应的大小调整策略,系统会根据系统运行情况修改新生代大小(-Xmn),Eden和Survivor比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等参数;来提供最合适的停顿时间和最大吞吐量
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集 停顿时间的-XXzMaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的: 系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、 每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占 总时间的5% (即1 / (1+19)),默认值为99,就是允许最大1% (即1 / (1+99))的垃圾收集时间。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。 除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy 。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄 (-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC自适应的调节策略(GC Ergonomics)。如果读者对于收集器运作原来不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。只需要把基本的内存数据设置好(如-Xmx 设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio (更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World, 对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
ParNew收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处, 但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial收集器外,目前只有它能与CMS收集器配合工作。在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器一 CMS收集器(Concurrent Mark Sweep),这款收集器是 HotSpot虚拟机中第一款真正意义上的并发(Concuirent)收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器 Parallel Scavenge配合工作,所以 在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个 。
ParNew收集器也是使用-XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于 GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑 CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于"标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤, 包括:
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标仅仅只是标记一下 GC Roots 能直接关联到的对象;速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通上图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,Sun公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause Collector)。但是CMS还远达不到完美的程度,它有以下3个明显的缺点:
-XX:UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThread=n,并行线程数,一般为cpu核心数,图中N=4。
-XX:ConcGCThreads=threads,一般设置为n/4,并发垃圾回收线程数。
-XX:CMSInitiatingOccupancyFraction=percent
-XX:CMSScavengeBeforeRemark 设置执行多少次不压缩的FULL GC后,执行一次带压缩的FULL GC
定义: Garbage First,jdk9后默认垃圾收集器。
适用场景
相关VM参数
将老年代再进行细分,分成一个个的 Card(一个大约512K),如果老年代其中有一个对象引用了新生代对象,就将它标记为 Dirty Card ,做GC Root 遍历的时候就不用遍历整个老年代了,而是只需要关注 Dirty Card 区域就可以了,这样可以减少搜索范围,提高扫描根对象效率。
新生代有一个叫 Remembered Set ,他会记录Incoming Reference(外部引用)也就是记录都有哪些脏卡,将来对Eden做垃圾回收的时候,可以先通过 Remembered Set 得到对应的 Dirty Card 在通过这些脏卡区域去遍历 GC Root 就减少了 GC Root 的遍历时间。
在每次对象引用发生变更时,post-write barrier更新脏卡(这是异步操作不会立即完成 Dirty Card 的更新,并把更新指令放入dirty card queue队列中,等待线程执行完成 Dirty Card 更新)
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
会对E、S、O进行全面垃圾收集
SerialGC
ParallelGC
CMS
G1
并发标记时:当对象的引用发生改变时,JVM会给他加一个写屏障(只要对象引用发生改变这个写屏障的代码就会被执行,并且将该对象加入到队列中),重新标记时线程就会将队列中的对象取出并检查。
pre-write barrier + satb_mark_queue
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s2 = new String("hello");//char[]{'h','e','l','l','o'}
将所有新分配的字符串放入一个队列,当新生代回收时,检查是否有重复的字符串,相同的字符串指向同样的字符数组char[]
注意 与String.intern()不同
所有对象在并发标记后,直到哪些类不在被使用,当类的实例都被回收掉并且、类加载器内所有的类都不再被使用,则卸载他加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark 默认开启
并发标记必须在堆空间沾满之前完成,否则触发FULL GC
JDK9之前使用-XX:InitiatingHeapOccupancyPercent 容易触发FULL GC 调小了垃圾回收频繁
JDK9 可以去动态调整
与其他GC收集器相比,G1具备如下特点:
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划 分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆根的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region (这也就是Garbage-First名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间以可以 获取尽可能高的收集效率。
G1把内存“化整为零”的思路,理解起来似乎很容易,但其中的实现细节却远远没有想象中那样简单,把Java堆分为多个Region 后,垃圾收集是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本 Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才有,只是在G1中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
G1的前几个步骤的运作过程和 CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS (Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可 用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发 执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变 动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面, 最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停 顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排 序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这 个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用 户可控制的,而且停顿用户线程将大幅提高收集效率。
官方调优文档地址:地址
“C:\Program Files\Java\jdk1.8.0_202\bin\java” -XX:+PrintFlagsFinal -version | findstr “GC”
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {
product}
uintx AutoGCSelectPauseMillis = 5000 {
product}
bool BindGCTaskThreadsToCPUs = false {
product}
uintx CMSFullGCsBeforeCompaction = 0 {
product}
uintx ConcGCThreads = 0 {
product}
bool DisableExplicitGC = false {
product}
bool ExplicitGCInvokesConcurrent = false {
product}
bool ExplicitGCInvokesConcurrentAndUnloadsClasses = false {
product}
uintx G1MixedGCCountTarget = 8 {
product}
uintx GCDrainStackTargetSize = 64 {
product}
uintx GCHeapFreeLimit = 2 {
product}
uintx GCLockerEdenExpansionPercent = 5 {
product}
bool GCLockerInvokesConcurrent = false {
product}
uintx GCLogFileSize = 8192 {
product}
uintx GCPauseIntervalMillis = 0 {
product}
uintx GCTaskTimeStampEntries = 200 {
product}
uintx GCTimeLimit = 98 {
product}
uintx GCTimeRatio = 99 {
product}
bool HeapDumpAfterFullGC = false {
manageable}
bool HeapDumpBeforeFullGC = false {
manageable}
uintx HeapSizePerGCThread = 87241520 {
product}
uintx MaxGCMinorPauseMillis = 4294967295 {
product}
uintx MaxGCPauseMillis = 4294967295 {
product}
uintx NumberOfGCLogFiles = 0 {
product}
intx ParGCArrayScanChunk = 50 {
product}
uintx ParGCDesiredObjsFromOverflowList = 20 {
product}
bool ParGCTrimOverflow = true {
product}
bool ParGCUseLocalOverflow = false {
product}
uintx ParallelGCBufferWastePct = 10 {
product}
uintx ParallelGCThreads = 8 {
product}
bool ParallelGCVerbose = false {
product}
bool PrintClassHistogramAfterFullGC = false {
manageable}
bool PrintClassHistogramBeforeFullGC = false {
manageable}
bool PrintGC = false {
manageable}
bool PrintGCApplicationConcurrentTime = false {
product}
bool PrintGCApplicationStoppedTime = false {
product}
bool PrintGCCause = true {
product}
bool PrintGCDateStamps = false {
manageable}
bool PrintGCDetails = false {
manageable}
bool PrintGCID = false {
manageable}
bool PrintGCTaskTimeStamps = false {
product}
bool PrintGCTimeStamps = false {
manageable}
bool PrintHeapAtGC = false {
product rw}
bool PrintHeapAtGCExtended = false {
product rw}
bool PrintJNIGCStalls = false {
product}
bool PrintParallelOldGCPhaseTimes = false {
product}
bool PrintReferenceGC = false {
product}
bool ScavengeBeforeFullGC = true {
product}
bool TraceDynamicGCThreads = false {
product}
bool TraceParallelOldGCTasks = false {
product}
bool UseAdaptiveGCBoundary = false {
product}
bool UseAdaptiveSizeDecayMajorGCCost = true {
product}
bool UseAdaptiveSizePolicyWithSystemGC = false {
product}
bool UseAutoGCSelectPolicy = false {
product}
bool UseConcMarkSweepGC = false {
product}
bool UseDynamicNumberOfGCThreads = false {
product}
bool UseG1GC = false {
product}
bool UseGCLogFileRotation = false {
product}
bool UseGCOverheadLimit = true {
product}
bool UseGCTaskAffinity = false {
product}
bool UseMaximumCompactionOnSystemGC = true {
product}
bool UseParNewGC = false {
product}
bool UseParallelGC := true {
product}
bool UseParallelOldGC = true {
product}
bool UseSerialGC = false {
product}
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
查看FULL GC前后的内存占用,考虑下面几个问题
新生代特点
新生代是否越大越好?
幸存区大到能保留(当前活跃对象 + 需要晋升的对象)
晋升阈值配置得当,使长时间存活对象尽快晋升
以cms为例
首先分析可能原因,先做新生代调优,新生代内存过小,对象放不下直接进入老年代,不仅Minor GC频繁,老年代中也会存入大量垃圾对象,FULL GC 也会频繁发生。
所以,适当增大新生代内存大小,使得Eden可以存下新生的多个对象,不会过于频繁触发Minor GC,幸存区存下幸存对象能够使幸存对象在新存区中保存,不会过早进入老年代,老年代内存占用减轻。
查看GC日志看哪个时间段花费时间多(一般是重新标记的时间多)
分析:请求高峰,并发用户很多,产生的新对象很多,堆中对象数目较大。CMS中重新标记会扫描整个堆内存来标记对象,所以会耗用大量时间。
调优:-XX:+CMSScavengeBeforeRemark 设置在重新标记前对新生代进行一次垃圾清理,会大大减少重新标记的时间。
在jdk1.7及以前,方法区以永久代方式实现,永久代内存不足时,也会发生FULL GC ,增加
jdk1.8及以后,利用元空间实现,使用的是系统内存,内存充裕,很少会触发FULL GC。