java对象引用与垃圾收集器回收算法
1.如何判断对象可回收
核心思想:堆内存中对象没有被任何引用。
在c语言中没有自动化垃圾回收机制,需要开发者自己人工清理堆垃圾,在java中开发自动化方式清理堆垃圾。
引用计数法
引用计数法:每次当该对象引用一次的时候,引用次数都会+1,如果引用的次数为0 则认为没有被引用,直接被垃圾给回收清理掉。
最大的缺陷:A如果引用B,B引用A 但是其他对象没有任何的引用A和B,相互存相互依赖,无法被垃圾回收。
Java虚拟机采用可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
白话文解释:只要对象的引用和GCRoot没有引用关系则可以进行垃圾回收,反之则不能够进行垃圾回收。解决了循环依赖的问题
哪些可以作为GC Roots
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.元空间中类静态属性引用的对象。
3.本地方法栈中JNI(即一般说的Native方法)引用的对象。
可以使用 MemoryAnalyzer 工具进行文件内存分析,下载地址,https://www.eclipse.org/mat/downloads.php
代码测试:
public class Test001 {
public static void main(String[] args) throws IOException {
ArrayList<Object> list = new ArrayList<>();
mayikts.add("aaa");
mayikts.add("bbb");
System.out.println("存储成功..");
System.in.read();
list = null;
System.out.println("list变为null");
System.in.read();
System.in.read();
System.out.println("end");
}
}
-- 打镜像文件命令
jmap -dump:format=b,live,file=a.bin 53120
2.java四种引用方法
2.1:强引用
-Xmx8m :设置堆内存
-XX:+PrintGCDetails -verbose:gc :打印 gc 回收信息
强引用:被引用关联的对象永远不会被垃圾收集器回收
Object object=new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
public class Test01 {
public static void main(String[] args) {
// 强引用
UserInfo userInfo1 = new UserInfo("ming");
UserInfo userInfo2 = userInfo1;
userInfo1 = null;
System.out.println(userInfo2);
}
}
控制台输出结果:com.example.javareference.UserInfo@77459877
2.2:软引用(SoftReference)
软引用:软引用关联的对象,在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常
public class Test02 {
public static void main(String[] args) {
soft1();
// soft2();
}
/**
* 软引用:打印已经被清理的对象
*/
private static void soft1() {
ArrayList<SoftReference<byte[]>> objects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SoftReference<byte[]> softReference = new SoftReference<>(new byte[4 * 1024 * 1024]);
System.out.println(softReference.get());
objects.add(softReference);
}
System.out.println("打印结果:");
objects.forEach((t) -> {
System.out.println(t.get());
});
}
/**
* 软引用:不打印已经被清理的对象
*/
private static void soft2() {
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
ArrayList<SoftReference<byte[]>> objects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SoftReference<byte[]> softReference = new SoftReference<>(new byte[4 * 1024 * 1024], referenceQueue);
objects.add(softReference);
}
Reference<? extends byte[]> poll = referenceQueue.poll();
while (poll != null) {
objects.remove(poll);
poll = referenceQueue.poll();
}
System.out.println("打印结果:");
objects.forEach((t) -> {
System.out.println(t.get());
});
}
}
2.3:弱引用(WeakReference)
无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
public class Test02 {
public static void main(String[] args) {
UserInfo userInfo1 = new UserInfo("ming");
// 弱引用
WeakReference<UserInfo> userInfoWeakReference = new WeakReference<>(userInfo1);
userInfo1 = null;
// 手动进行 gc 回收
System.gc();
System.out.println(userInfoWeakReference.get());
}
}
控制台输出结果:null
2.4:虚引用(PhantomReference)
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
public class Test04 {
public static void main(String[] args) {
UserInfo userInfo1 = new UserInfo("ming");
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 虚引用
PhantomReference<UserInfo> phantomReference = new PhantomReference<>(userInfo1, referenceQueue);
System.out.println(phantomReference.get());
}
}
控制台输出为:null
引用队列(ReferenceQueue)
软弱虚对应引用的指针放入到引用队列中,实现清理。
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
ArrayList<SoftReference<byte[]>> objects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SoftReference<byte[]> softReference = new SoftReference<>(new byte[4 * 1024 * 1024], referenceQueue);
objects.add(softReference);
}
3.垃圾收集器回收算法
垃圾收集器在什么时候触发垃圾收集?
当新生代或者是老年代内存满的情况下,开始触发垃圾收集。
3.1 标记清除算法
根据gcRoot对象的引用链,发现如果该对象没有被引用的情况下,则标记为垃圾,然后在清除。
优点:算法非常简单。
缺点:空间地址不连续、空间利用率不高 容易产生碎片化。
标记清除之前效果图:
绿色代表未被引用的空间,黄色代表被引用的空间。
标记清除之后效果图:
白色代表标记清除的空间,出现了空间不连续的情况,而这些不连续的空间只能存储少于等于自身空间大小的对象,所以空间利用率比较低。
3.2 标记整理算法
根据gcRoot对象的引用链,发现如果该对象没有被引用的情况下,则标记为垃圾。
标记整理与标记清除区别在于:避免标记清除算法产生的碎片问题,清除垃圾过程中,会将可用的对象实现移动,内存空间更加具有连续性。
优点:没有内存的碎片问题,空间地址保持连续。
缺点:整理过程中会产生内存地址移动,在移动过程中其他线程无法访问堆内存,效率偏低。Stop-The-World
效果图如下所示:
3.3 标记复制算法
当我们堆内存触发gc的时候,在from区中将可用对象拷贝到to中,在直接将整个from区清空,依次循环切换。
优点:不会产生内存碎片
缺点:比较占内存空间,有二块空间分为两个区域:from 和to区 相等。
标记复制之前效果图:
标记复制之后,经过清理后效果图:
4.分代算法
对我们堆内存空间实现分代,核心分为新生代、老年代。在整个堆内存中,新生代触发GC回收次数比老年代多。
4.1:新生代
刚创建的对象都存在新生代空间,存放生命周期较短的对象的区域。新生代中分为eden区、from区、to区。默认情况下,新生代中eden区、from区、to区比例为8:1:1.
刚创建的对象存放在eden区,当eden区满的时候,幸存的对象晋升存放到from区或者是to区,from区和to区使用的是标记复制算法。
4.2:老年代
存放生命周期较长的对象的区域。
哪些对象会晋升到老年代中
1:年限达到。当 GC 多次回收的时候,如果一直引用能达到一个阈值的情况下,直接晋升到老年代。阈值可以自己设置
例子:假如有二个对象A、B经过多次的新生代GC回收之后依然还被引用的话,就会晋升到老年代。
2:大对象。如果存放的对象的内存大于新生代空间的内存,则直接存放到老年代中。
例子:假如新生代空间为5M、老年代空间为10M,如果这时候创建的对象为6M,这时候对象则会直接存放到老年代空间中。如果创建的对象为11M,则会报OM(内存溢出)异常。
相同点: 都存储在Java堆上。
不同点:新生代触发的是MinorGC、老年代触发的是FullGC。默认情况下,新生代与老年代存储空间比例为1:2。
4.3:程序发生内存溢出的原因
存放对象的空间大小大于我们老年代的空间大小。
4.4:为什么在分代算法中,新生代有from区和to区且大小一样
因为新生代中 GC 触发非常频繁,为了能够更加高效的清理垃圾,所以采用标记复制算法。
4.5:分代算法中,老年代为什么使用标记整理算法
因为在老年代空间满的情况下会触发 FullGC进行垃圾回收,FullGC 垃圾回收会同时回收新生代、老年代、元空间三个区域。
代码演示:
-Xms18m -Xmx18m -XX:+PrintGCDetails -verbose:gc
设置堆初始空间、最大空间为20M,并在控制台打印出详细的垃圾回收信息。
18M空间:新生代与老年代空间比例为1:2,新生代占6M ,老年代占12M。
1 public class Test05 { 2 3 public static void main(String[] args) { 4 // -Xms18m -Xmx18m -XX:+PrintGCDetails -verbose:gc 5 ArrayList<Object> objects = new ArrayList<>(); 6 objects.add(new byte[1024 * 1024 * 15]); 7 } 8 } 9 10 11 控制台输出结果: 12 [GC (Allocation Failure) [PSYoungGen: 2617K->504K(5632K)] 2617K->1011K(17920K), 0.0016494 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 13 [GC (Allocation Failure) [PSYoungGen: 504K->488K(5632K)] 1011K->1031K(17920K), 0.0006205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 14 [Full GC (Allocation Failure) [PSYoungGen: 488K->0K(5632K)] [ParOldGen: 543K->981K(12288K)] 1031K->981K(17920K), [Metaspace: 3203K->3203K(1056768K)], 0.0069905 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 15 [GC (Allocation Failure) [PSYoungGen: 0K->0K(5632K)] 981K->981K(17920K), 0.0003778 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 16 [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(5632K)] [ParOldGen: 981K->953K(12288K)] 981K->953K(17920K), [Metaspace: 3203K->3203K(1056768K)], 0.0062126 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 17 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 18 at com.example.javareference.Test05.main(Test05.java:17) 19 Heap 20 PSYoungGen total 5632K, used 301K [0x00000000ffa00000, 0x0000000100000000, 0x0000000100000000) 21 eden space 5120K, 5% used [0x00000000ffa00000,0x00000000ffa4b5b8,0x00000000fff00000) 22 from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) 23 to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) 24 ParOldGen total 12288K, used 953K [0x00000000fee00000, 0x00000000ffa00000, 0x00000000ffa00000) 25 object space 12288K, 7% used [0x00000000fee00000,0x00000000feeee4c0,0x00000000ffa00000) 26 Metaspace used 3292K, capacity 4496K, committed 4864K, reserved 1056768K 27 class space used 358K, capacity 388K, committed 512K, reserved 1048576K
由上面控制台打印信息看出第11、12、14行进行 GC 垃圾回收在新生代区域(PSYoungGen)。
第13、15行进行 FullGC 垃圾回收,包括 新生代(PSYoungGen)、老年代(ParOldGen)、元空间(Metaspace) 三个区域。
[PSYoungGen: 2617K->504K(5632K)]:表示新生代空间回收之前是2617k,回收之后是504k,5632k是整个新生代空间的大小。等于20行:total 5632K。
19行以下打印的是 新生代、老年代、元空间的内存使用情况。
5.GC日志分析工具
GCViewer、GCEasy、GCHisto、GCLogViewer 、Hpjmeter、garbagecat