JVM03------垃圾收集(下)
Posted hermioner
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM03------垃圾收集(下)相关的知识,希望对你有一定的参考价值。
一. 什么是GC
Java与C语言相比的一个优势是,可以通过自己的JVM自动分配和回收内存空间。
垃圾回收机制是由垃圾收集器Garbage Collection来实现的,GC是后台一个低优先级的守护进程。在内存中低到一定限度时才会自动运行,因此垃圾回收的时间是不确定的。
为何要这样设计:因为GC也要消耗CPU等资源,如果GC执行过于频繁会对Java的程序的执行产生较大的影响,因此实行不定期的GC。
与GC有关的是:JVM运行时数据区中的堆(对象实例会存储在这里)和 gabagecollector方法。
垃圾回收GC只能回收通过new关键字申请的内存(在堆上),但是堆上的内存并不完全是通过new申请分配的。还有一些本地方法,这些内存如果不手动释放,就会导致内存泄露,所以需要在finalize中用本地方法(nativemethod)如free操作等,再使用gc方法
System.gc();
二. 什么是垃圾
Java中那些不可达的对象就会变成垃圾。对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,从这些树根往下搜索,搜索走过的链称为引用链。
当一个对象到GC Roots没有任何引用链相连时,则证明这个对象为可回收的对象。
可以作为GC Roots的主要有以下几种:
(1)栈帧中的本地变量表所引用的对象。
(2)方法区中类静态属性和常量引用的对象。
(3)本地方法栈中JNI(Native方法)引用的对象。
//垃圾产生的情况举例: //1.改变对象的引用,如置为null或者指向其他对象 Object obj1 = new Object(); Object obj2 = new Object(); obj1 = obj2; //obj1成为垃圾 obj1 = obj2 = null ; //obj2成为垃圾
//2.引用类型 //第2句在内存不足的情况下会将String对象判定为可回收对象,第3句无论什么情况下String对象都会被判定为可回收对象 String str = new String("hello"); SoftReference<String> sr = new SoftReference<String>(new String("java")); WeakReference<String> wr = new WeakReference<String>(new String("world"));
//3.循环每执行完一次,生成的Object对象都会成为可回收的对象 for(int i=0;i<10;i++) { Object obj = new Object(); System.out.println(obj.getClass()); }
//4.类嵌套 class A{ A a; } A x = new A();//分配了一个空间 x.a = new A();//又分配了一个空间 x = null;//产生两个垃圾
//5.线程中的垃圾 calss A implements Runnable{ void run(){ //.... } } //main A x = new A(); x.start(); x=null; //线程执行完成后x对象才被认定为垃圾
三. Java引用类型划分
引用类型可以说是整个Java开发的灵魂所在,如果没有合理的引用操作,那么就有可能产生垃圾问题。引用也需要一些合理化的设计,再很多时候并不是所有的对象需要一直被使用。
JDK1.2之后关于引用提出了四种方案:
- 强引用:当内存不足的时候,JVM宁可出现OutOfMermeory,也需要保存,并且不会将此空间回收。Object obj=new Object();
- 软引用:当内存不足的时候,进行对象的回收处理。往往用于高速缓存中。
- 弱引用:不管内存是否紧张,只要有垃圾产生了,就立即回收
- 虚引用(幽灵引用):和没有引用是一样的
在我们目前接触中,只需要接触到强引用。
1. 强引用
它是JVM默认支持的引用模式,即:在引用期间内,如果该内存被指定的栈内存引用,那么该对象就无法被GC回收。一旦出现了内存空间不足,就会出现“OOM”错误。
举例:观察强引用
public class Test { public static void main(String args[]) { Object object=new Object();//强引用 Object ref=object;//引用传递 object=null;//断开了一个连接,断开了obj到new object的连接,但是ref还在引用它 System.gc(); System.out.println(ref); } }
运行结果发现对象还在。就说明了,如果堆内存有一个栈内存指向,那么该对象将无法被GC回收。
强引用是我们一直在使用的模式,并且也是以后的开发之中主要的使用模式。正因为强引用有这样的内存分配问题,因此尽量少实例化对象。
2. 软引用
在许多的开源组件中,往往会使用软引用作为缓存组件出现。它最大的特点就是在内存空间不足的时候回收,充足就不回收。如果想要实现软引用,则需要有一个单独的类来实现控制:java.lang.ref.SoftReference。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
举例:观察软引用
public class Test { public static void main(String args[]) { Object object=new Object(); SoftReference<Object> reference=new SoftReference<Object>(object); object=null;//断开连接 System.gc(); System.out.println(reference.get()); } }
Obj=null的执行本来会让new Object()成为垃圾,可以因为软引用ref的存在,使用了这个obj对象,所以,它不会被回收。现在内存不紧张。
如果要弄成内存紧张,可以配置VM参数来测试,输出将会是null
3. 弱引用
弱引用的本质含义就是只要已进行GC,就会被立即回收,不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些弱引用的对象。弱引用需要使用的是MAP接口的子类(java.util.WeakHashMap)。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
举例:观察弱引用
public class Test { public static void main(String args[]) { String key=new String("hello"); String value=new String("world"); Map<String, String> map=new WeakHashMap<>(); map.put(key, value); System.out.println(map); key=null; System.out.println(map); System.gc(); System.out.println(map); } }
{hello=world} {hello=world} {}
只要一旦出现GC,则必须进行回收处理。
使用WeakHashMap和HashMap的区别
前者是弱引用,后者是强引用。
Java.lang.ref.WeakRefernce(这个弱引用类相较于上面weakhashmap更少用)
观察这个弱引用类:
public class Test { public static void main(String args[]) { String key=new String("hello"); WeakReference<String> reference=new WeakReference<String>(key); System.out.println(reference.get()); key=null; System.out.println(reference.get()); System.gc(); System.out.println(reference.get()); } }
hello hello null
我们一般不使用弱引用。它之所以不敢轻易使用的原因就是因为其本身一旦有了GC之后,就会立刻清空,这不利于程序的开发。(我们有个弱引用的概念就行了)
4. 引用队列
它指的是保存那些准备被回收的对象。很多得时候所有的对象的回收扫描都是从根对象开始的。那么对于整个GC而言,如果想要确定哪些对象可以被回收,那么就需要确定好引用的强度,这个也就是所谓得引用路径的设置。
若果现在要找到对象5,那么很明显1找到5是(强+软),2到5是(强+弱),软引用要比弱引用保存得更强一些,所以这个时候对于对象得引用而言,如果要进行引用得关联得判断,就必须找到强关联。那么为了避免非强关联得引用对象带来得内存问题,所以有一个引用队列的概念。如果在创建软引用或者弱引用的时候使用了引用队列得方式,那么这个对象被回收得时候会自动保存在队列之中。
举例:使用引用队列
弱引用中假如queue表示对象被回收之后会存在队列里面
public class Test { public static void main(String args[]) throws InterruptedException { Object object=new Object(); ReferenceQueue<Object> queue=new ReferenceQueue<>(); WeakReference<Object> reference=new WeakReference<Object>(object,queue); System.out.println(queue.poll()); object=null; System.gc(); Thread.sleep(200); System.out.println(queue.poll()); } }
null [email protected]
(note:对象保存到引用队列之中会需要一定时间,因此,观察后一个打印,需要等一会儿)
这种引用队列只是进行了一些被回收对象得控制,意义不大。
5. 虚引用(幽灵引用)
永远取得不了幽灵数据
举例:观察幽灵引用
public class Test { public static void main(String args[]) throws InterruptedException { Object object=new Object(); ReferenceQueue<Object> queue=new ReferenceQueue<>(); PhantomReference<Object> reference=new PhantomReference<Object>(object, queue); System.gc(); System.out.println(reference.get()); System.out.println(queue.poll()); } }
null null
虽然没有断强引用连接,但是输出还是Null,幽灵总是为Null
所有在幽灵引用类型中得数据都不会真正得保留。
(我们开发中关心最多得就是强引用)
四. 典型的垃圾回收算法
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。
下面讨论几种常见的垃圾收集算法。
1. Mark-Sweep(标记-清除)算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。
标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发GC。
2. Coping(复制)算法
Copying算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把第一块内存上的空间一次清理掉,这样就不容易出现内存碎片的问题,并且运行高效。
但是该算法导致能够使用的内存缩减到原来的一半。而且,该算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。(这也是为什么后面提到的新生代采用Copying算法)
3. Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。
该算法标记阶段标记出所有需要被回收的对象,但是在完成标记之后不是直接清理可回收对象,而是将存活的对象都移向一端,然后清理掉端边界以外的所有内存(只留下存活对象)。
....还有别的。上面三种正式年轻代,老年代之前介绍的回收算法。
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,该算法效率在新生代也较高。但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(比例8:1:1),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和A空间。在进行了第一次GC之后,使用的便是Eden space和B空间了,下次GC时会将存活对象复制到A空间,如此反复循环。
当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,对象年龄达到15时,就会移动到老年代中。一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:byte[] data = newbyte[4*1024*1024]。
当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。这些搬运工作都是GC完成的,GC不仅负责在Heap中搬运实例,同时负责回收存储空间。
最后,因为每次回收都只回收少量对象,所以老年代一般使用的是标记整理算法。
Minor GC是新生代Copying算法。MinorGC触发条件:
(1)当Eden区满时,触发Minor GC。
Full GC的老年代,采取的Mark-Compact。Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行。
(2)老年代空间不足。
(3)方法区空间不足。
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
五. 典型的垃圾回收器
G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
参考文献:
https://blog.csdn.net/SEU_Calvin/article/details/51404589
《深入理解JAVA虚拟机》
李兴华老师的《Java内存模型》
以上是关于JVM03------垃圾收集(下)的主要内容,如果未能解决你的问题,请参考以下文章
03JVM调优调优--2.基础:参数说明(堆参数说明),垃圾收集器(前置知识:垃圾回收算法,GC种类),垃圾收集器(种类,相关介绍)