如果一个对象被另一个对象引用,那么它的引用计数加一,如果那个对象不再引用它了,那么引用计数减一。当引用计数为 0 时,该对象就应该被垃圾回收了。
但是下面这种互相引用的情况就无法回收了:
两个对象的计数都为1,导致两个对象都无法被释放
垃圾回收之前,扫描所有对象,判断每个对象是否被根对象引用,如果没有被根对象引用,那么在以后垃圾回收时就将那些没有与根相连的对象回收
查找可以作为GCRoot的对象:
运行下面程序:
public static void main(String[] args) throws IOException {
ArrayList<Object> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add(1);
System.out.println(1);
System.in.read();
list = null;
System.out.println(2);
System.in.read();
System.out.println("end");
}
使用 jmap -dump:format=b,live,file=1.bin 进程id
将堆内存中的信息存储到文件1.bin中
使用Eclipse Memory Analyzer 打开1.bin文件,选择 GC Root 选项进行分析:
可以看出 GC Root 分为四类:
强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
例如:C对象和B对象强引用了 A1 对象,只有 C 对象和 B 对象都不强引用了 A1 对象,A1 对象才可以被垃圾回收
Cleaner 对象虚引用 ByteBuffer 对象
当ByteBuffer 对象被回收后,其分配的直接内存还没有被回收。
这时Cleaner对象会被放入引用队列:
虚引用所在的队列会由一个ReferenceHandler的线程来定时的在队列中寻找是否有新入队的 Cleaner ,如果有就会调用 Cleaner 对象中的 clean 方法。而 clean 方法就会根据前面记录的直接内存的地址,用 Unsafe.freeMemory 方法释放直接内存:
所有对象都继承自Object类,其中有一个 finalize 方法
如果 finalize 方法被重写了,也就是被对象实现了。jvm 就会将终结器引用对象放入引用队列。就会有一个FinalizeHandler的线程对队列进行检查。找到了终结器引用,就会根据它找到该对象,调用 finalize 方法。调用完后,第二次垃圾回收才可以把该对象回收掉
内存不足时,会回收软引用对象,可用于对象缓存
可以用 SoftReference 对象保证要软引用的对象:
首先限制内存大小的 vm option :-Xmx20m
public class d1_SoftReference {
public static void main(String[] args) {
// list -> SoftReference -> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024 * 4]);
System.out.println(reference.get());
list.add(reference);
System.out.println(list.size());
}
System.out.println("循环结束" + list.size());
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
输出:可以看出由于内存空间不足,前三个ref都被回收掉了
[B@10f87f48
1
[B@b4c966a
2
[B@2f4d3709
3
[B@4e50df2e
4
[B@1d81eb93
5
循环结束5
null
null
null
[B@4e50df2e
[B@1d81eb93
软引用,引用的对象为 null,那么软引用本身也就没有必要保留了。这里可以用软引用队列来对软引用对象本身进行回收
public class d1_SoftReference {
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[]被回收时,软引用本身会被加入到 queue中
SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024 * 4], queue);
System.out.println(reference.get());
list.add(reference);
System.out.println(list.size());
}
// 清除掉没有内容的软引用本身:
Reference<? extends byte[]> poll = queue.poll();
while (poll != null){
list.remove(poll);
poll = queue.poll();
}
System.out.println("循环结束" + list.size());
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
输出:这里的list中只有2个byte数组了
[B@10f87f48
1
[B@b4c966a
2
[B@2f4d3709
3
[B@4e50df2e
4
[B@1d81eb93
5
循环结束2
[B@4e50df2e
[B@1d81eb93
不管内存是否充足,如果只有弱引用引用该对象,就会回收该对象
// -Xmx20m
public class d2_WeakReference {
public static void main(String[] args) {
// list -> WeakReference -> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[1024*1024*4]);
list.add(ref);
for (WeakReference<byte[]> weakReference : list) {
System.out.println(weakReference.get() + " ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
输出:在垃圾回收时会将弱引用对象回收:
[B@10f87f48
[B@10f87f48
[B@1d81eb93
[B@10f87f48
[B@1d81eb93
[B@7291c18f
null
null
null
[B@34a245ab
null
null
null
null
null
循环结束:5
Mark Sweep
记录垃圾对象的地址
优点:速度快
缺点:造成内存碎片,本可以存下的新对象因为内存分布的太分散而无法存下
将所有存活的对象移动到一端,避免了内存碎片的产生,但是由于对象发生了移动,所以算法的速度慢
把内存空间分为两部分。一部分内存空间用完了,就把存活的对象复制到另一部分的内存空间上面。然后把使用过的内存空间进行清理。
对于大对象,新生代无法存入,就会直接存入老年代
如果大对象老年代都无法存下,就会抛出内存溢出的异常
多线程下运行,一个线程的内存溢出不会影响其他线程中断
-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
晋升详情
-XX:+PrintGCDetails -verbose:gc
GC详情
-XX:+ScavengeBeforeFullGC
FullGC 前 MinorGC
串行
多线程运行
响应时间优先
VM 参数:-XX:+UseSerialGC=Serial+SerialOld
Serial : 工作在新生代,回收算法是复制算法
SerialOld:工作在老年代,回收算法是标记整理算法
所有线程在安全点前阻塞,单线程的垃圾回收器运行;因为可能会更改对象地址,所以线程需要阻塞(STW)
-XX:+UseParallelGC
:新生代采用复制算法
-XX:+UseParallelOldGC
:老年代采用标记整理了算法
Paralle
表示回收器是并行的
垃圾回收器的线程数默认是和CPU的核数相同的。
-XX:+UseAdaptiveSizePolicy
:自动调整垃圾回收的参数,例如eden,from,to,晋升阈值的大小
-XX:GCTimeRation=ratio
: 1 / ( 1 + r a t i o ) 1/(1+ratio) 1/(1+ratio) ,ratio 默认99,垃圾回收的时间占比为1%;但是难以达到,一般设置19;调整垃圾回收时间与总时间的占比,用于调整吞吐量
-XX:MaxGCPauseMillis=ms
:默认为 200 ms;用于调整每次垃圾回收的暂停时间
-XX:+UseConcMarkSweepGC
:并发标记清除回收器,用于老年代;
-XX:+UseParNewGC
:新生代的复制算法
如果并发出现问题,例如标记清除算法会产生很多内存碎片就会并发失败,老年代的垃圾回收器就会退化成 SerialOld
的复制串行回收器
老年代空间不足,达到运行点,进行初始标记,对那些根对象进行遍历
随后进行并发标记遍历其他对象,与此同时其他线程继续运行
当进行重新标记时,需要STW, 随后进行重新标记
ParallelGCThreads=n
-XX:ConcGCThreads=threads
:可以设置并发标记线程数量;清理垃圾的同时,可能会产生新垃圾,得等到下次垃圾回收时才能释放,称为浮动垃圾。所以需要为这些浮动垃圾预留空间。
-XX:CMSInitiatingOccupancyFraction=percent
:执行垃圾回收时的内存占比
-XX:+CMSScavengeBeforeRemark
:重新标记时,可能新生代的对象引用老年代的对象,就需要扫描整个堆进行可达性分析,而这些新生代的对象将来也是要被回收掉的,就导致做了无用的查找工作。这个参数可以在重新标记前,对新生代进行一次垃圾回收,这样新生代的对象就少了,查找的工作就少了
Garbage First 简称 G1 收集器。
2017 JDK 9默认
适用场景:
相关 JVM 参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
新生代内存紧张后,就会将新生代的Eden区域的垃圾回收对象复制到幸存区 survivor
当 survivor 区域的年龄超过晋升阈值就会复制到老年区 Old
在Young GC时会进行 GC Root的初始标记
老年代占用堆空间达到阈值时,进行并发标记(不会STW),由下面的 VM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent
(默认45%)
对 E,S,O 进行全面垃圾回收
Eden 中的对象会复制到 Survivor ,Surivior 区域年龄达到晋升阈值会复制到 Old
Old 区域会根据设置的垃圾回收时间有选择的回收部分老年代的对象,挑选那些回收价值高的也就是能释放内存更多的对象进行回收。
垃圾回收暂停时间设置:-XX:MaxGCPauseMillis=ms
新生代回收的跨代引用(老年代引用新生代)问题
新生代回收时需要查找有哪些 GC Root,再进行可达性分析和回收
查找 GC Root 需要查找整个老年代花费时间多。
可以把老年代区域分割成多个 card,如果这个 card 引用了新生代,那么这个 card 就是脏 card。
pre-write barrier + satb_mark_queue
C的引用发生改变则会添加写屏障,并放入队列,将来的Remark阶段就会通过队列对C对象进行处理决定是否垃圾回收
优点:节省大量内存
缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
开启这个开关(默认打开):-XX:+UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark
默认启用
-XX:InitiatingHeapOccupancyPercent
-XX:InitiatingHeapOccupancyPercent
用来设置初始值低延迟还是高吞吐量,选择合适的回收器
响应时间优先:CMS,G1,ZGC
吞吐量优先:ParallelGC
limit n
限制一下查出数据的数量新生代特点:
新生代内存是否越大越好?
-Xmn
:设置新生代内存的vm命令
Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
新生代能容纳所有【并发量 * (请求-响应)】的数据
幸存区大到能保留【当前活跃对象+需要晋升对象】
晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
以 CMS 为例,具有浮动垃圾的问题,如果浮动垃圾存不下就会并发失败退化为Serial
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
:重新标记前回收一遍新生代的垃圾