1.如何判断对象可以被回收
在判断对象是否可以被回收的方法由许多,以下我们来简单介绍几种。
1.引用计数法
引用计数法实现的原理:在对象中添加一个引用计数器,每当对对象引用时,就把计数器的值加1,而失去引用就减1。任何时刻只要引用计数器值变为0,就表示对象不可能再被使用,就对对象进行回收。python就使用了这种方法。
但是虽然引用计数法,实现简单,判断效率很高,但是无法解决循环引用的问题。故Java不使用这种方法对,来判断对象是否可以被回收。
2.可达性分析算法
Java及现在许多商用型语言都使用可达性分析算法来判断对象是否可被回收。
算法原理:以一系列称为“GC root”的对象作为起始点。从这些点开始向下搜索。搜索走过的路径称为“引用链”。当一个对象到Gc root没有任何引用链时,(图论称为不可达状态)就表明该对象是不可用的,可以被垃圾回收器回收。
Java中GC root包括以下对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即native修饰的方法)引用的对象
2.四种引用
用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2以前, Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜.”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
在JDK1.2之后, Java对引用的概念进行了扩充,将引用分为强引用( Strong Reference )、软引用( Soft Reference )、弱 引用( Weak Reference )、虚 引用( Phantom Reference ) 4种,这4种引用强度依次逐渐减弱。
强引用(Strong Reference)
强引用在Java中普遍存在,类似new出来的用引用都是强引用。只有GC root对象都不通过强引用引用对象时,对象才能被回收。
软引用(Soft Reference)
- 只有软引用引用对象时,如果在垃圾回收后,内存仍然不够,再次执行垃圾回收时回收软引用对象
- 可配合引用队列来释放软引用自身
软引用(Weak Reference)
- 只有软引用引用对象时,执行垃圾回收,无论内存是否充足都会回收软引用对象
- 可以配合引用队列来是否软引用自身
虚引用(Phantom Reference)
虚引用是几种引用中最弱的引用,必须配合引用队列使用。主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放内存。
除了以上几种引用,Java中还有一种类似以上的引用
终结器引用(Final Reference)
无需内部编码,但必须配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时未被回收)。再由Finalizer线程通过终结器引用找到被引用对象,并调用其finalize方法。第二次执行垃圾回收时才能被回收。
软引用使用的实例
/**
* -Xmx40m -XX:+PrintGCDetails -verbose:gc
*/
public class SfotrefDemo {
private static int SIZE = 2*1024*1024;
public static void main(String[] args) throws IOException {
// test();
test2();
}
private static void test() throws IOException {
List<Byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
System.out.println(i);
list.add(new Byte[SIZE]);
}
System.in.read();
}
/**
* 演示软引用的使用
* @throws IOException
*/
private static void test2() throws IOException {
List<SoftReference<Byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
// System.out.println(i);
SoftReference<Byte[]> reference = new SoftReference<>(new Byte[SIZE]);
System.out.println(reference.get());
list.add(reference);
System.out.println(list.size());
}
System.out.println("循环结束....");
for (SoftReference<Byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
其中test()是普通加载,而test2()使用了软引用。为了更好的演示垃圾回收机制所以把heap堆的大小设置为了40m,而且开启了gc回收日志
输出结果:
[Ljava.lang.Byte;@4554617c
1
[Ljava.lang.Byte;@74a14482
2
[Ljava.lang.Byte;@1540e19d
3
[GC (Allocation Failure) [PSYoungGen: 2341K->808K(11776K)] 26917K->25392K(39424K), 0.0011902 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Ljava.lang.Byte;@677327b6
4
[GC (Allocation Failure) --[PSYoungGen: 9204K->9204K(11776K)] 33788K->33788K(39424K), 0.0030469 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 9204K->8847K(11776K)] [ParOldGen: 24584K->24577K(27648K)] 33788K->33424K(39424K), [Metaspace: 3468K->3468K(1056768K)], 0.0090574 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 8847K->8847K(11776K)] 33424K->33424K(39424K), 0.0033271 secs] [Times: user=0.14 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 8847K->0K(11776K)] [ParOldGen: 24577K->638K(16896K)] 33424K->638K(28672K), [Metaspace: 3468K->3468K(1056768K)], 0.0046308 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Ljava.lang.Byte;@14ae5a5
5
循环结束....
null
null
null
null
[Ljava.lang.Byte;@14ae5a5
Heap
PSYoungGen total 11776K, used 8703K [0x00000000ff300000, 0x0000000100000000, 0x0000000100000000)
eden space 10240K, 84% used [0x00000000ff300000,0x00000000ffb7fdb8,0x00000000ffd00000)
from space 1536K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000ffe80000)
to space 1536K, 0% used [0x00000000ffe80000,0x00000000ffe80000,0x0000000100000000)
ParOldGen total 16896K, used 638K [0x00000000fd800000, 0x00000000fe880000, 0x00000000ff300000)
object space 16896K, 3% used [0x00000000fd800000,0x00000000fd89fa10,0x00000000fe880000)
Metaspace used 3475K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 382K, capacity 388K, committed 512K, reserved 1048576K
可以看到,在三个软引用对象进入list之后内存就已经不够了,此时调用了一次gc回收新时代的空间。而把第四个对象放入后内存彻底不够,所以调用了full GC回收内存。最终使之前存放的四个对象空间都被释放。只剩下第五个对象存储在list中。最后新生代占84%的内存,而老年代只占3%。
引用队列
上面软引用的例子中我们虽然回收了软引用,引用的内存空间,但是软引用自身对象我们也是不需要的,却没有被删除。如果想要删除被回收引用的软引用对象。那么就需要使用引用队列。
Java中引用队列为ReferenceQueue类
例子:
/**
* -Xmx40m
*/
public class SfotrefDemo {
private static int SIZE = 2*1024*1024;
public static void main(String[] args) throws IOException {
test2();
}
/**
* 演示软引用的使用
* @throws IOException
*/
private static void test2() throws IOException {
List<SoftReference<Byte[]>> list = new ArrayList<>();
ReferenceQueue<Byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// System.out.println(i);
SoftReference<Byte[]> reference = new SoftReference<>(new Byte[SIZE],queue);
System.out.println(reference.get());
list.add(reference);
System.out.println(list.size());
}
System.out.println("循环结束....");
System.out.println("=========");
Reference<? extends Byte[]> poll = queue.poll();
while (poll != null){
System.out.println(poll);
list.remove(poll);
poll = queue.poll();
}
System.out.println("=========");
for (SoftReference<Byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
可以看到,只要在生成软引用对象时把queue放入构造方法,那么被回收的软引用对象会自动存入引用队列中。
输出结果:
[Ljava.lang.Byte;@4554617c
1
[Ljava.lang.Byte;@74a14482
2
[Ljava.lang.Byte;@1540e19d
3
[Ljava.lang.Byte;@677327b6
4
[Ljava.lang.Byte;@14ae5a5
5
循环结束....
=========
java.lang.ref.SoftReference@7f31245a
java.lang.ref.SoftReference@6d6f6e28
java.lang.ref.SoftReference@135fbaa4
java.lang.ref.SoftReference@45ee12a7
=========
[Ljava.lang.Byte;@14ae5a5
可以看到被回收内存的软引用对象,都被删除了。其他引用如弱引用以可使用引用队列进行回收引用对象。
弱引用的使用
/**
* -Xmx40m -XX:+PrintGCDetails -verbose:gc
*/
public class WeakrefDemo {
private static int SIZE = 2*1024*1024;
public static void main(String[] args) throws IOException {
List<WeakReference<Byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
WeakReference<Byte[]> reference = new WeakReference<>(new Byte[SIZE]);
System.out.println(reference.get());
list.add(reference);
System.out.println(list.size());
for (WeakReference<Byte[]> weakReference : list) {
System.out.print(weakReference.get() + " ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
可以看到弱引用和软引用的使用方法大致相同。不过根据上述的两种引用的特点。它们触发回收的机制不同。所以我们来看看执行上述代码会发生什么
[Ljava.lang.Byte;@4554617c
1
[Ljava.lang.Byte;@4554617c
[Ljava.lang.Byte;@74a14482
2
[Ljava.lang.Byte;@4554617c [Ljava.lang.Byte;@74a14482
[Ljava.lang.Byte;@1540e19d
3
[Ljava.lang.Byte;@4554617c [Ljava.lang.Byte;@74a14482 [Ljava.lang.Byte;@1540e19d
[GC (Allocation Failure) [PSYoungGen: 2341K->856K(11776K)] 26917K->25440K(39424K), 0.0014177 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Ljava.lang.Byte;@677327b6
4
[Ljava.lang.Byte;@4554617c [Ljava.lang.Byte;@74a14482 [Ljava.lang.Byte;@1540e19d [Ljava.lang.Byte;@677327b6
[GC (Allocation Failure) [PSYoungGen: 9252K->888K(11776K)] 33836K->25472K(39424K), 0.0009754 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Ljava.lang.Byte;@14ae5a5
5
[Ljava.lang.Byte;@4554617c [Ljava.lang.Byte;@74a14482 [Ljava.lang.Byte;@1540e19d null [Ljava.lang.Byte;@14ae5a5
循环结束:5
Heap
PSYoungGen total 11776K, used 9591K [0x00000000ff300000, 0x0000000100000000, 0x0000000100000000)
eden space 10240K, 84% used [0x00000000ff300000,0x00000000ffb7fdf0,0x00000000ffd00000)
from space 1536K, 57% used [0x00000000ffe80000,0x00000000fff5e010,0x0000000100000000)
to space 1536K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000ffe80000)
ParOldGen total 27648K, used 24584K [0x00000000fd800000, 0x00000000ff300000, 0x00000000ff300000)
object space 27648K, 88% used [0x00000000fd800000,0x00000000ff002030,0x00000000ff300000)
Metaspace used 3475K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 382K, capacity 388K, committed 512K, reserved 1048576K
可以看到,内存不够执行垃圾回收时,弱引用会把直接释放引用的内存。
不过弱引用自身对象并未被删除,如果想删除弱引用自身对象。可以使用引用队列。和软引用使用方法相同。
3.垃圾回收算法
标记-清除
标记清除算法的具体实现思想是:第一步标记,标记出需要回收的对象(不能从GC root对象到达的对象)。第二步清除,把标记的对象的起始地址和结束地址存入空间空间列表中。就可以完成清除。
虽然标记-清除算法的执行简单,效率高,但是该算法会产生大量的空间碎片,导致空间的浪费。
可以看到,标记-清除算法产生了大量空间碎片。在程序分配较大的对象时无法找到足够的内存从而再一次执行垃圾回收动作。
标记-整理
标记-整理算法和标记-清除算法一样分两步执行:第一步也是对需要回收的对象进行标记。但第二步和标记-清除算法不同。标记-整理算法的整理步骤是:然存活的对象向一端移动。然后直接清理掉端边界以外的内存。
可以看到,标记-整理算法不会产生大量空间碎片。从而避免了标记-清除算法的缺点。
但是标记-整理算法也有缺点:
它的整理步骤会涉及大量的移动操作。回收的速度慢。
复制
为了解决效率问题,复制算法出现。它将内存按容量划分为大小相同的两部分。每次只使用其中一块。当一块用完就把所用的活着的对象复制到另一块,然后把已使用的空间一次清除。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
算法牺牲空间换时间上的高效。
缺点:只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
复制算法的执行过程如下。
在现实情况中,依据条件的不同,相应的使用不同的回收算法。
4.分代垃圾回收
当前商业虚拟机的垃圾收集都采用“分代收集”(GenerationalCollection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保, 就必须使用“标记一清理”或者“标记一整 理”算法来进行回收。
–《深入了解Java虚拟机》周志明
分代垃圾回收具体流程:
- 对象先分配在伊甸园中。
- 新生代内存不足时,触发一次minor gc,将伊甸园和from中的存活对象复制到to中,并把存活对象年龄加1。然后进行内存空间的回收。最后交换from和to。
- 在minor gc的过程中会引发stop the world(STW)。暂停其他用户线程,等待垃圾回收结束。用户线程恢复运行。
- 当对象寿命超过阈值,会晋升到老年代。最大寿命是15(4bit)因为寿命存储在对象头中,而对象头给寿命分配到空间只有4bit。
- 当老年代内存不足时,会先尝试触发minor gc,如果空间仍然不够,则会触发full gc。STW时间更长。
JVM相关参数及其含义
分代回收案例
在开始案例讲解之前先开一个代码:
/**
* -Xms20M -Xmx20M -Xmn1OM -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
* 设置堆初始大小,最大大小,设置新生代的大小,使用SerialGC回收垃圾(因为Java内置垃圾回收器会自动改变新生代和老年代的大小),开启gc日志
*/
public class HeapDemo2 {
private static int _512k = 512 * 1024;
private static int _1m = 1024 * 1024;
private static int _6m = 6 * 1024 * 1024;
private static int _7m = 7 * 1024 * 1024;
private static int _8m = 8 * 1024 * 1024;
public static void main(String[] args) {
}
}
该程序的输出为:
Heap
//新生代9216k因为to空间不会存储数据,所以在新生代计算可用空间时,没计算to的大小
def new generation total 9216K, used 2330K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
//伊甸园8192k
eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee46988, 0x00000000ff400000)
//from1024k
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
//老年代10m
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
//元空间不在堆内,此处打印出来仅供参考
Metaspace used 3426K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 375K, capacity 388K, committed 512K, reserved 1048576K
例1:
public class HeapDemo2 {
//机器不同内存的占用情况也不同,可以自己测试来决定写入多大内存
private static int _512k = 512 * 1024;
private static int _1m = 1024 * 1024;
private static int _2m = 2 * 1024 * 1024;
private static int _3m = 3 * 1024 * 1024;
public static void main(String[] args) {
List<Byte[]> list = new ArrayList<>();
list.add(new Byte[_1m]);
list.add(new Byte[_512k]);
}
}
输出:
[GC (Allocation Failure) [DefNew: 6262K->661K(9216K), 0.0040394 secs] 6262K->4757K(19456K), 0.0040708 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 2791K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
from space 1024K, 64% used [0x00000000ff500000, 0x00000000ff5a54b0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3471K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 381K, capacity 388K, committed 512K, reserved 1048576K
可以看到:当新生代内存不足时,在没有等待对象年龄到达阈值,就把对象存入老年代了
例2:
public class HeapDemo2 {
private static int _512k = 512 * 1024;
private static int _1m = 1024 * 1024;
private static int _2m = 2 * 1024 * 1024;
private static int _3m = 3 * 1024 * 1024;
public static void main(String[] args) {
List<Byte[]> list = new ArrayList<>();
list.add(new Byte[_2m]);
// list.add(new Byte[_512k]);
}
}
输出:
Heap
def new generation total 9216K, used 2330K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee46988, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3470K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 381K, capacity 388K, committed 512K, reserved 1048576K
当一个对象的大小超过新生代内容纳的大小,而小于老年代的内存大小时,大对象会被直接存入老年代。
例3:
public class HeapDemo2 {
private static int _512k = 512 * 1024;
private static int _1m = 1024 * 1024;
private static int _2m = 2 * 1024 * 1024;
private static int _3m = 3 * 1024 * 1024;
public static void main(String[] args) {
// List<Byte[]> list = new ArrayList<>();
// list.add(new Byte[_2m]);
// list.add(new Byte[_512k]);
new Thread(() ->{
List<Byte[]> list = new ArrayList<>();
list.add(new Byte[_3m]);
}).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
[GC (Allocation Failure) [DefNew: 4199K->863K(9216K), 0.0015575 secs][Tenured: 0K->861K(10240K), 0.0022580 secs] 4199K->861K(19456K), [Metaspace: 4339K->4339K(1056768K)], 0.0038617 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 861K->805K(10240K), 0.0018371 secs] 861K->805K(19456K), [Metaspace: 4339K->4339K(1056768K)], 0.0018624 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at wf.memo.HeapDemo2.lambda$main$0(HeapDemo2.java:24)
at wf.memo.HeapDemo2$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Heap
def new generation total 9216K, used 1233K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 15% used [0x00000000fec00000, 0x00000000fed34510, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 805K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 7% used [0x00000000ff600000, 0x00000000ff6c97e0, 0x00000000ff6c9800, 0x0000000100000000)
Metaspace used 4853K, capacity 4978K, committed 5248K, reserved 1056768K
class space used 544K, capacity 591K, committed 640K, reserved 1048576K
子线程的内存溢出不会影响主线程。虚拟机栈。
5.垃圾回收器
串型
- 单线程的垃圾回收器
- 适用于内存较小的个人电脑
开启串行的JVM参数
-XX:UseSerialGC=Serial+SerialOld
其中Serial工作在新生代,使用了复制算法,SerialOld工作中老年代,采用了标记-整理算法。
如下演示了串行的工作流程:
开始时cpu正常工作,当运行一段时间后,内存不够需要进行垃圾回收时,先到达一个安全点,停下正在运行的线程,然后开启垃圾回收线程。在完成垃圾回收后,在恢复正在阻塞的线程。
吞吐量优先
- 多线程
- 堆内存较大,需要多核cpu’
- 让单位时间内,STW的时间最短(0.2 + 0.2 =0.4)
开启吞吐量优先的JVM参数:
//这两个参数,只要开启其中一个就会连带开启另一个参数,前一个管理新生代,后一个管理老年代是JVM默认开启的,即该回收器就是Java默认的垃圾回收器
-XX:+UseParallelGC~-XX:+UseParallel0ldGC
//该参数表明采用的是自适应的调整新生代的大小。
-XX:+UseAdaptiveSizePolicy
//该参数调整堆得大小从而影响垃圾回收的时间。具体的计算公式为:1/(1+ratio),其中ratio默认值为99.带入可计算结果为0.01,表示在100秒内,只有1秒可用于垃圾回收。如果时间过大可增大堆的大小减少垃圾回收的次数,降低垃圾回收的频率。不过实际中0.01的回收率比较难以达到,所以一般情况ratio的值都被设置为19
-XX :GCTimeRatio=ratio
//参数设置了一次垃圾回收需要的最大时间。默认200ms。该参数与上一个参数冲突,因为是一个参数想要降低垃圾回收的频率就需要增大堆的大小,而堆空间变大那么一次垃圾回收的时间就会增加。所以在生产环境我们需要根据实际情况来确定这两个参数
-XX:MaxGCPauseMillis=ms
//控制在垃圾回收时开启的线程数
-XX :ParallelGCThreads=n
吞吐量优先的垃圾回收的具体流程
开始是与串行工作相同,即需要垃圾回收时,先把所有正在运行的线程达到一个安全的后停下。但吞吐量优先会开启多个线程来进行垃圾回收,提高垃圾回收的效率。从而降低整体垃圾回收的时间。开启的垃圾回收线程数在默认情况下和cpu的核数相关,cpu几核就开启几个垃圾回收线程。不过可以通过上述的参数来自己设置垃圾回收线程的个数。
响应时间优先(CMS)
- 是多线程
- 堆内存较大,多核cpu
- 尽可能让单次STW时间最短(0.1+0.1+0.1+0.1+0.1=0.5).即每次回收速度快,但是总体效率可能不如吞吐量优先
CMS是一种基于标记清除的算法
开启该回收器的参数为:
//该参数表明开启响应时间优先垃圾回收器,从其参数名称中可以Conc看出该回收器是并发进行垃圾回收。不过在并发失败的情况下ConcMarkSweepGC会退化到SerialOld回收器
- XX:+UseConcMarkSweepGC~-XX:+UseParNewGC~Serial0ld
//该参数前一个在吞吐量中讲过,这里不在赘述,第二个参数表明在并发执行垃圾回收时要开启几个线程,默认为前一个参数的四分之一。其他四分之三线程留给用户线程工作。
- XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads
//参数是设置当达到某一个值如90%时进行垃圾回收,因为CMS是因为用户线程同时进行的,需要预留一部分空间给浮动垃圾。该参数默认值为65%,参数值越小,CMS的触发的时机就越早
- XX:CMSInitiatingOccupancyFraction=percent
//是一个开关,其意义是在remark前进行一次老年代的垃圾回收。因为remark扫描新生代的引用时,新生代可能会引用老年代,而一些对象又是可以清除的,进行一次老年代垃圾回收后可以降低扫描所占用时间。条故效率
- XX:+CMSScavengeBeforeRemark
工作流程:
在也感到要垃圾回收时先阻塞其他线程进行一次初始标记,此时标记的是GCroot所以占用时间很短。之后恢复一部分工作线程,只留少部分垃圾回收线程对要回收的对象进行标记。当内存占用达到上述设置的点时,停止用户线程,对所有对象重新进行一次标记。因为前一阶段的标记过程中,用户线程也在并发执行,可能会产生新的垃圾。在重新标记完成后,让用户线程和垃圾回收线程并发执行。进行垃圾回收。
CMS提高了整个相同的响应时间,但是也减低了系统的吞吐量,因为在运行过程中一部分资源被用于了,垃圾回收。
CMS的最大缺点是,它采用了标记-清除的回收方式,会产生大量内存碎片,当新产生的对象过大不能存入内存时,就会出现并发失败的问题。此时CMS就会转换为SerialOld回收器整理内存空间。然后再进行CMS垃圾回收,这个过程会耗费大量时间。
G1
定义:Garbage frist
- 2004年论文发表
- 2009JDK 6u14开始尝试使用
- 2012JDK7u4官方支持
- 2017JDK9默认使用
特性:
- 同时注重吞吐量(Throughput) 和低延迟(Low latency),默认的暂停目标是200 ms
- 超大堆内存,会将堆划分为多个大小相等的Region(大小为2的次方mb),每个Region都可以作为伊甸园和老年代。
- 整体上是标记+整理算法,两个区域(Region)之间是使用复制算法
相关JVM参数:
//开启G1回收器。jkd9默认使用无需进行开启
-XX:+UseG1GC
//设置Region区域的大小,size=1,2,4,8...
-XX:G1HeapRegionSize=size
//设置最大暂停时间
-XX:MaxGCPauseMillis=time
G1回收的三个阶段
G1垃圾回收器有三个阶段新生代回收(Young Collection)、新生代+并发标记(YoungCollection + Concurrent Mark)和混合回收(Mixed Collection),下面我们对这三个区域进行说明。
1)Young Collection
开始运行一段时间后,在Yong Collection时会触发新生代的垃圾回收,会存在STW(时间很短)
新生代垃圾回收会把存活对象存入存活区
当存活对象到达阈值,会被存入老年代,不过年龄的会被拷贝到其他幸存区。
Young Collection的回收过程就是简单的分代回收。
2)Young Collection + CM
这是G1回收器的第二阶段,有以下过程
- Young GC时进行GC root的初始标记
- 老年代内存空间使用达到一定阈值时会触发并发标记,(不会STW)阈值由以下JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
3)Maxed Conection
是G1回收的第三阶段会对E(伊甸园),S(幸存区)和O(老年代)进行全面的垃圾回收。
- 最终标记(Remark),会STW
- 拷贝存活(Evacuation),会STW
它会对新生代和幸存区进行全部的回收,把该回收的回收,该升入幸存区和老年代的升入。在对老年代进行回收时,因为G1算法也要****有***-XX:MaxGCPauseMillis=time时间限制,*****不可能回收所有老年代区域,所以G1通过算法算出回收价值最大(能回收内存最多的区域),的几个老年代区域进行回收。**这也是该垃圾回收器被称为Garbage Frist的原因。当然在回收结束后会把幸存的老年代对象复制进入新的老年代区域。
4) Full GC
SerialGC
- 新生代内存不足发生的垃圾收集- minor gc
- 老年代内存不足发生的垃圾收集- full gc
ParallelGC
- 新生代内存不足发生的垃圾收集- minor gc
- 老年代内存不足发生的垃圾收集- full gc
CMS
- 新生代内存不足发生的垃圾收集- minor gc
- 老年代内存不足
G1
- 新生代内存不足发生的垃圾收集- minor gc
- 老年代内存不足:CMS和G1一样在回收的内存,不能正常供应使用时,会发生并发异常。把垃圾回收器进行降级到SerialOld,此时SerialOld回收器会触发Full GC。
5)Young Collection的跨代引用问题
在对新生代对象进行标记时,可能会有老年代的对象对其进行引用,而从老年代内存中查询存在哪些引用时,会耗费大量时间。使用就引用了卡(card)技术对老年代分卡。每个卡的大小约为512k。把引用了新生代对象的老年代对象所在的卡,标记为脏卡。中查询哪些老年代对象引用新生代对象时,访问脏卡就行,这样大大提升了标记的效率。
其具体实现为:
- 设置卡表和Rememberd set(记录哪些老年代对象对新生代对象进行引用,即记录脏卡)
- 在引用变更时进行异步操作,通过post -write barrier + dirty card queue(写屏障,并把要变更引用的对象,存入dirty card queue中异步进行变更引用)
- concurrent refinement threads开启一个并发线程来更新Remembered Set
6)Remark
我们知道G1的第一次标记是与用户线程并发进行的,所以有可能在标记完成后,被标记为要回收的对象,被一个GC root引用,此时该对象的标记就是错误的,因为此时对象不能被回收,却被标记为可回收的,这个问题会在remark阶段解决。
上图黑色表示已经处理过,不能被回收。灰色表示正在处理。白色表示未被处理或可被回收空间。如果在一次标记完成后
c被标记为可回收对象。而此时a引用了c
那么在可回收对象引用被改变时,会触发一次pre-write barrier屏障,把c插入一个stab_mark_queue队列
在remark时期把c变为不可回收。
G1垃圾回收器的一些优化
1)JDK8u20的字符串去重
在JKD8u20中G1对新生代的垃圾回收时,做出了一些优化。即如果新生代的字符串对象中如果串值相同,那么就会使这些字符串对象指向同一个char数值(字符串在底层是以字符数组存储)
-XX:+UseStringDeduplication
该功能默认开启,字符串对象去重优化。
例:
String s1 = "hello";//char[]{'h','e','l','l','o'}
String s2 = "hello";//char[]{'h','e','l','l','o'}
G1垃圾回收器会把新分配的字符串存入一个队列中。在新生代的垃圾回收中使s1和s2指向同一个char数组以回收多余的数组来,减少空间的使用。
和intern()方法的不同;
- String.intern()关注的是字符串对象,而G1关注的是字符数组
- 在JVM内部,二者使用了不同的字符串表
2)JDK8u40并发标记类卸载
所有对象经过并发标记后,就知道哪些类不再被使用。当一个类加载器,加载的所以类都不在被使用就会卸载它加载的所以类。
-XX:+ClassUnloadingWithConcurrentMark 默认启用
3) JDK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
4) JDK 9并发标记起始时间的调整
并发标记必须在堆空间占满前完成,否则退化为FullGC
- JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent确定可使用空间的阈值
- JDK 9可以动态调整-XX:InitiatingHeap0ccupancyPercent 用来设置初始值。在使用过程中进行数据采样并动态调整阈值,以减少Full GC的产生。而且总会添加一个安全的空档空间。
6.垃圾回收调优
最快的GC是不发生GC
查看FullGC前后的内存占用,考虑下面几个问题:
- 数据是不是太多?
resultSet = statement.executeQuery(“select * from大表”)会返回表中所有数据,如果表中数据过多会用法GC可以使用linit "select * from大表 limit n"减少一次查询带来的数据
- 数据表示是否太臃肿?
- 对象图
- 对象大小。包装类型和基本类型的占用空间就不一样16 Integer 24 int 4
- 是否存在内存泄漏?如使用static Map map =大量存储数据,导致GC甚至内存溢出
- 软,若引用
- 把要缓存的数据由第三方缓存实现。如redis
新生代的调优
新生代的特点:
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer每个线程都有自己的私有空间来创建内存
- 死亡对象的回收代价是零
- 大部分对象用过即死
- 前三点导致。Minor GC的时间远远低于Full GC
故对新生代回收内存的性价比最高。
一般来说如果新生代内存回收的性价比最高,那么就把新生代设置到越多越好。但是新生代真的越大越好?
新生代内存不断加大引发的问题:
- 新生代内存空间越大,那么老年代的内存就越少。而老年代的内存空间满后,会触发full GC。所以新生代内存不是越大越好。
- 新生代空间越大,那么一次minor gc使用的时间就越大。会影响性能。因为minor gc的回收分标记,和复制两个阶段。如果新生代空间过大,那么复制就会耗费大量时间
官方给出的建议是:新生代内存大于25%。小于50%。
而在使用中发现新生代能容纳所有[并发量* (请求响应)]的数据,最好,因为一次请求响应,新生代中的数据大多都会把回收,所以这个大小刚好能满足新生代的使用,而又不会触发Full gc。
新生代幸存区大到能保留[当前活跃对象+需要晋升对象],不能太小,太小垃圾回收器会动态调整幸存区向老年代晋升的阈值。导致许多存在时间不久的对象,升入老年代。导致内存浪费。
晋升阈值配置得当,让长时间存活对象尽快晋升。配置阈值的参数如下
-XX:MaxTenuringThreshold=threshold
-XX: +PriptTenuringDistribution:该参数打印幸存区对象的年龄信息
示例:
Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
-age2:1:1366864 bytes,30358888 total ,
- age 3: 1425912 bytes, 31784800 total
老年代内存调优
以CMS为例
- CMS的老年代内存越大越好,可以预留足够的空间给浮动垃圾
- 先尝试不做调优,如果没有Full GC那么说明老年代空间足够,发生full gc先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~ 1/3
- -XX: CMSInitiat ingOccupancyFraction=percent该参数表明在老年代空间使用percent%后进行一次垃圾回收