Java内存回收机制
1.java的内存
java的内存结构分为
- 堆 (是gc的主要区域) 线程共享,主要是用于分配实例对象和数组
- 栈 线程私有,它的生命周期和线程相同,又分成 虚拟机栈和本地方法栈,只有它会报 StackOverFlowError,栈深度超标
- 方法区 线程共享 用于储存被虚拟机加载的类的信息,静态变量 常量和编译后的.class字节码
- 程序计数器 线程私有,线程之间不相互影响,独立存取;
以上部分,线程私有是不会发生gc.并且他们是随线程生随线程灭,即程序计数器 本地方法栈和虚拟机栈
2.GC回收机制--判断是否可以gc
- 引用计数算法
原理:通过一个计数器对对象进行计数,对象被引用时+1,引用失效时-1;当计数为0时则说明可以被回收;
缺点:很难解决对象的相互循环引用问题 - 可达性分析算法
Java虚拟机所采用的算法;
原理:通过一些列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
那么哪些对象可以被称为gc roots呢----虚拟机栈(栈中的本地变量列表)/方法区静态属性/方法区常量引用/本地方法栈中JNI 所引用的的对象都是可以作为 gc roots的
深入理解分代回收算法 Survivor(幸存者) Eden (谷歌翻译为伊甸园)
- 复制算法中内存划分其实并不是按照1:1来划分老年代和新生代,,而是按照8:1:1分一个大的Eden区和两个小的survivor的空间
- 为什么需要2个Survivor区 新生代一般经历15次Gc就可以移到老年代.当第一次gc时,我们可以把Eden的存活对象放入Survivor A空间,第二次Gc时,Survivor A也要使用复制算法,存活对象放到Survivor B上,第三次gc时,又将Survivor B对象复制到Survivor A上如此循环往复;
- 为什么Eden这么大,因为新生代中存活的对象,需要转移的Survivor 的对象不多,算是缓解了复制算法的缺点;
4.GC回收机制--gc的执行机制
- Scavenge GC
当新对象生成并且在Eden申请空间失败时就会触发Scavenge GC;Eden区的gc会比较频繁 - Full GC
是对整个堆进行清理,要比Scavenge GC要慢,什么情况要进行Full GC呢,如下四种:
持久代被写满
System.gc调用
老年代被写满
上一次GC之后Heap的各域分配策略动态变化-
Java虚拟机内存原型
寄存器:我们在程序无法控制
栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是堆中
存取速度比堆块,仅次于寄存器,栈数据可以共享,栈的数据大小与生存期必须是确定的,缺乏灵活性。
堆:存放new产生的数据
可以动态分配内存大小,生存期也不必事先告诉编译器,因为它在运行时动态分配内存,Java的垃圾收集器会自动收走这些不再使用的数据,但缺点是,由于在运行时分配内存,存取速度较慢
静态域:存放在对象中用static定义的静态成员
常量池:存放常量(利用final关键字修饰的)
非RAM存储:硬盘等永久存储空间Java引用的种类
>对象在内存中状态
对于JVM的垃圾回收机制来说,如果一个对象,没有一个引用指向它,那么它就被认为是一个垃圾。那该对象就会回收。可以把JVM内存中对象引用理解成一种有向图,把引用变量、对象都当成有向图的顶点,将引用关系当成图的有向边(注意:有向边总是从引用变量指向被引用的Java对象)
1、可达状态
当一个对象被创建后,有一个或一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可以通过引用变量来调用该对象的方法和属性。
2、可恢复状态
如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态,在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存空间,在回收该对象之前,系统会调用所有对象的finalize方法进行资源的清理,如果系统在调用finalize方法重新让一个引用变量引用该对象,则这个对象会再次变为激活状态,否则该 对象状进入不可达状态。
3、不可达状态
当对象与所有引用变量的关联都被切继,且系统已经调用所有对象的finalize方法依然没有该对象变成可达状态,那这个对象将永久性地失去引用,最后变成不可达状态,只有当一个对象处于不可达状态时统才会真正回收该对象所占有的资源。*对象的状态转换图如下:
>强引用
强引用是Java编程中使用广泛的引用类型,被强引用所引用的Java对象绝不会被垃圾回收,当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题,因此强引用是造成Java内存泄漏的主要原因之一
*代码示例class Person { String name; int age; public Person(String name,int age) { this.name=name; this.age=age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } } public class ReferenceTest { public static void main(String[] args) { //创建一个长度为10000的强引用数组,来保存10000个Person对象 Person[] person=new Person[10000]; //依次初始化 for(int i=0;i<person.length;i++) { person[i]=new Person("名字"+i,(i+1)*2%100); } System.out.println(person[1]); System.out.println(person[3]); //通知系统进行垃圾回收 System.gc(); System.runFinalization(); System.out.println(person[1]); System.out.println(person[3]); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
运行结果(结果说明内存还是足够的)
我们来把修改java虚拟机内存,把堆内存减少到2m
(操作:程序右键选属性->run/debug settings->选中应用程序->编辑->Arguments->VM arguments输入框输入 -Xmx2m -Xms2m )
再运行(程序因为内存不足而中止)>软引用
软引用通过SoftReference类来实现,当系统内存空间足够,软引用的对象不会被系统回收,程序也可以使用该对象,当系统内存不足时,系统将会回收
*代码示例// 创建一个长度为10000的弱引用数组,来保存10000个Person对象 SoftReference<Person>[] person = new SoftReference[10000]; // 依次初始化 for (int i = 0; i < person.length; i++) { person[i] = new SoftReference<Person>(new Person("名字" + i, (i + 1) * 2 % 100)); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
运行结果(结果说明内存足够的)
修改java虚拟机内存,把堆内存减少到2m,再运行
从运行结果可以看出,内存不足时,垃圾回收机制会回收SoftReference引用的对象>弱引用
弱引用与软引用有点相似,区别是弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
*代码示例//创建一个字符串对象 String str=new String("疯狂Java讲义"); //创建一个弱引用,让该弱引用引用到“疯狂Java讲义”字符串对象 WeakReference<String> wr=new WeakReference<String>(str); //切断str引用变量和“疯狂Java讲义”字符串对象之间的引用关系 str=null; //取出弱引用所引用的对象 System.out.println(wr.get()); //强制垃圾回收 System.gc(); System.runFinalization(); //再次取出弱引用所引用的对象 System.out.println(wr.get());
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
内存分配图
运行结果
从运行结果看出,当系统垃圾回收机制启动后,弱引用的对象就会被清除掉。null表面该对象已经被清除了>虚引用
虚引用通过PhantomReference类实现,类似没有引用,主要作用是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用的对象是否即将被回收,虚引用不能单独使用,必须要和引用队列(ReferenceQueue)联合使用
*代码示例//创建一个字符串对象 String str=new String("这是虚引用"); //创建一个引用队列 ReferenceQueue<String> rq=new ReferenceQueue<String>(); //创建一个虚引用,让该虚引用引用到“这是虚引用”字符串对象 PhantomReference<String> pr=new PhantomReference<String>(str,rq); //切断str引用与"这是虚引用"字符串之间的引用 str=null; //取出虚引用所引用的对象 System.out.println(pr.get()); //强制垃圾回收 System.gc(); System.runFinalization(); //取出引用队列中最先进入队列中引用与pr进行比较 System.out.println(rq.poll()==pr);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
运行结果
从运行结果可以看出,在未强制进行垃圾回收,程序输出null,说明系统无法通过虚引用访问被引用的对象,当程序强制回收垃圾后,虚引用引用的对象被回收,然后该引用会添加到关联的引用队列中,所以输出true,所以说程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收
Java的内存泄漏
无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费,就是内存泄漏
1、静态变量引起内存泄露:
根据分代回收机制(后面有讲),JVM会将程序中obj引用变量存在Permanent代里,这导致Object对象一直有效,从而使obj引用的Object得不到回收
例:class Person { static Object obj=new Object(); }
- 1
- 2
- 3
- 4
2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
例:public static void main(String[] args) { Set<Person> set = new HashSet<Person>(); Person p1 = new Person("唐僧","pwd1",25); Person p2 = new Person("孙悟空","pwd2",26); Person p3 = new Person("猪八戒","pwd3",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素! p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变 set.remove(p3); //此时remove不掉,造成内存泄漏 set.add(p3); //重新添加,居然添加成功 System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素! for (Person person : set) { System.out.println(person); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
3、监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。4、各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。5、内部类和外部模块等的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
public void registerMsg(Object b);
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
6、单例模式
单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子:class A{ public A(){ B.getInstance().setA(this); } .... } //B类采用单例模式 class B{ private A a; private static B instance=new B(); public B(){} public static B getInstance(){ return instance; } public void setA(A a){ this.a=a; } //getter... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况
垃圾回收机制
垃圾回收的基本算法
标记压缩法
先从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清除未标记的对象,而是将所有的存活对象压缩到内存的一端之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
标记回收法
从“GC Roots”(GC Roots指垃圾收集器的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象)集合开始,将内存整个遍历一次,保留所有被GC Roots直接或者间接引用到的对象,而剩下的对象都当作垃圾对待并回收,这个算法需要中断进程内其他组件的执行并且可能产生碎片化
复制回收法
将内存分为大小相等的两部分(假设A、B两部分),每次呢只使用其中的一部分(这里我们假设为A区),等这部分用完了,这时候就将这里面还能活下来的对象复制到另一部分内存(这里设为B区)中,然后把A区中的剩下部分全部清理掉。这样内存碎片的问题就解决了
分代回收法
根据对象的生命周期将内存划分,然后进行分区管理,在Java虚拟机分代垃圾回收机制中,应用程序可用的堆空间可以分为年轻代与老年代,年轻代有被分为Eden区,From区与To区
分代回收法更详细链接http://blog.csdn.net/sinat_36246371/article/details/52998505>堆内存的分代回收
>与垃圾回收的附加选项
下面两个选项用于设置java虚拟机内存大小
-Xms :设置java虚拟机堆内存的最大容量如java -Xmx256m XxxClass
-Xms :设置java虚拟机堆内存的初始容量,如java -Xms128m XxxClass下面选项都是关于java垃圾回收的附加选项
-xx:MinHeapFreeRatio =40 :设置java堆内存最小的空闲百分比,默认为40,如java -xx:MinHeapFreeRadio = 40 XxxClass-xx:MaxHeapFreeRatio=70 :设置Java堆内存最大的空闲百分比,默认为70,如java -XX:MaxHeapFreeRatio =70 XxxClass
-xx:NewRatio=2 ;设置Yonng/Old内存的比例,如java -XX:NewRatio=1 XxxClass
-xx:NewSize=size:设置Yonng代内存的默认容量,如java -XX:Newsize=64m XxxClass-xx:SurvivorRatio = 8;设置Yonng代中eden/survivor的比例,如java -xx:MaxNewSize=128m XxxClass
注意 当设置Young代的内存超过了-Xmx设置的大小时,Young设置的内存大小将不会起作用,JVM会自动将Young代内存设置为与-Xmx设置的大小相等。
-XX:PermSIze=size;设置Permnanent代内存的默认容量,如java –XX:PermSize=128m XxxClass
-XX:MaxPermSize=64m;设置Permanent代内存的最大容量,如java -XX:MaxPermSize=128m XxxClass
>常见垃圾回收器
- 串行回收器(Serial Garbage Collector)
Serial Garbage Collector通过暂停所有应用的线程来工作。它是为单线程工作环境而设计的。它中使用一个线程来进行垃圾回收。这种暂停应用线程来进行垃圾回收的方式可能不太适应服务器环境。它最适合简单的命令行程序。
通过 -XX:+UseSerialGC 参数来选用Serial Garbage Collector。 - Parallel Garbage Collector
Parallel Garbage Collector也被称为吞吐量收集器(throughput collector)。它是Java虚拟机的默认垃圾收集器。与Serial Garbage Collector不同,Parallel Garbage Collector使用多个线程进行垃圾回收。与Serial Garbage Collector相似的地方时,它也是暂停所有的应用线程来进行垃圾回收。
3. CMS Garbage CollectorConcurrent Mark Sweep (CMS) Garbage Collector使用多个线程来扫描堆内存来标记需要回收的实例,然后再清除被标记的实例。CMS Garbage Collector只有在如下两种情景才会暂停所有的应用线程:
当标记永久代内存空间中的对象时; 当进行垃圾回收时,堆内存同步发生了一些变化。
相比Parallel Garbage Collector,CMS Garbage Collector使用更多的CPU资源来确保应用有一个更好的吞吐量。如果分配更多的CPU资源可以获得更好的性能,那么CMS Garbage Collector是一个更好的选择,相比Parallel Garbage Collector。
通过 XX:+USeParNewGC 参数来选用CMS Garbage Collector。
内存管理的小技巧
>尽量使用直接量
当需要使用字符串,还有Byte,Short、integer、Long、Float、Double、Boolean、Character包装类的实例时,不应该采用new的方式来创建对象,而应该使用直接量来创建它们
应该是String str="hello";
而不是String str=new String("hello");
后者除了在创建一个缓存在字符串缓冲池的“hello”字符串,str所引用的String对象底层还包含一个存放了h、e、l、l、o的char[ ]数组>使用StringBuilder和StringBuffer进行字符串连接
String代表字符序列不可变的字符串,StringBuilderheStringBuffer都代表字符序列可变的字符串
建议StringBuilder st = new StringBuilder(); c = st.append(a).append(b);
- 1
- 2
不建议
String c = a+b;
- 1
因为这样会在运行的时候生成大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降
如果使用少量的字符串操作,使用 (+运算符)连接字符串;
如果频繁的对大量字符串进行操作,则使用
1:全局变量或者需要多线程支持则使用StringBuffer;
2:局部变量或者单线程不涉及线程安全则使有StringBuilder>尽早释放无用对象的引用
>尽量少用静态变量
>避免在经常调用的方法、循环中创建Java对象
>缓存经常使用的对象
>尽量不要使用finalize方法
>考虑使用SoftReference
参考:《疯狂java 突破程序员基本功的16课》
- 转自http://blog.csdn.net/jyxmust/article/details/54609602
-