【java】垃圾回收

来源:互联网 发布:图书数据 编辑:程序博客网 时间:2024/06/05 14:10

一、JVM内存结构

        java虚拟机在解析执行java程序的时候会把其管理的内存主要分成五块数据区域。

  • 程序计数器

        占用很少的内存空间,可以看做当前程序所执行的字节码的行号指示器,程序计数器通过改变计数的值来告知JVM选取下一条执行的指令,因为多线程中多个线程共享CPU时钟,为了不致使执行错乱,因此每个线程都有独立的程序计数器内存,该片内存为线程私有。

  • java虚拟机栈

        俗称栈区,描述方法执行的内存模型,当一个方法被调用执行的时候,方法会被压入栈创建栈帧,用于存放变量、局部变量、操作数栈、动态链接、方法出口等信息,因为不同线程操作的方法不一样,即便是操作同一个方法执行的进度也不一样,因此不可能共享同一片栈内存,所以该片内存也是线程私有。

  • 本地方法栈

        本地方法栈和java虚拟机栈的作用类似,不过虚拟机栈服务于java方法,而本地虚拟机栈则服务于native方法。

  • 堆区

        主要用于存放对象实例(几乎所有的对象实例都会在堆区中分配内存),是虚拟机管理的内存中最大的一块,允许线程共享该片内存区域,也是java GC的主要内存区域。

  • 方法区

        用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码等数据,该片内存也允许线程共享。

二、JAVA GC机制

        java垃圾回收的主要区域就是堆内存,样想探究java垃圾回收机制,首先要知道什么样的对象会被虚拟机回收内存,接着虚拟机什么时候开始垃圾回收。

  • 什么样的对象会被回收

        当一个对象初始化完成之后,栈内存中存放其引用类型,而堆内存中存放其实例数据,对应该类的类信息则被加载存放到方法区中(可以参看类加载器一文),初始化完成之后的实例对象会经历三种状态,分别是可达状态、可恢复状态和不可达状态,也只有当一个实例对象变成不可达状态之后,才会被java垃圾回收机制真正的回收内存。

        1. 可达状态:实例对象存在直接引用,形如Object o = new Object();,说明该实例数据在堆区中占用的内存还在被引用、访问;

        2. 可恢复状态:当实例对象失去引用的时候则进入可恢复状态,形如o = null;,此时java垃圾回收机制会标记该片内存为可回收,但在真正的执行内存回收操作之前会调用该对象的finalize方法(该方法定义在Object中),如果执行finalize方法使得对象恢复引用,则对象变回可达状态,否则对象进入不可达状态,等待垃圾回收;

        3. 不可达状态:对象失去引用,并且垃圾回收调用该对象的finalize方法之后没有使该对象变回可达状态,则该对象进入不可达状态,此时对象永久性的失去引用,只能等待被java垃圾回收占用的内存。

  • 什么时候回收

        当java虚拟机认为内存紧张的时候,虚拟机才会进行垃圾回收,而虚拟机何时开始垃圾回收对程序员透明。java垃圾回收会自动执行,但是由虚拟机决定何时执行,因此对程序员而言何时进行垃圾回收则不可预知。

  • 强制垃圾回收

        当一个对象失去引用后,会进入可恢复状态,此时如果调用其finalize方法没有使其重回可达状态则该对象会永久失去引用等待java GC回收内存系统什么时候回收内存对程序程序员是透明的由jvm选择何时进行回收但是程序中可以调用以下两种方式来进行强制内存回收但是这种强制对jvm来说其实是一种建议”,即便是写了强制回收的代码jvm也不一定会立即回收内存资源。

        1. System.gc();

        2. Runtime.getRuntime().gc();

        如以下代码示例,验证强制垃圾回收其实只是对虚拟机进行“建议”,而虚拟机什么时候开始真正回收内存无法预测。

package gc;public class Man {private static int count = 0;private String name;private int age;private String gender;public Man(String name, int age, String gender) {// TODO Auto-generated constructor stubthis.name = name;this.age = age; this.gender = gender;}@Overrideprotected void finalize() throws Throwable {// TODO Auto-generated method stubcount++;System.out.println("我被gc调用了"+count+"次!");}}
package gc;public class Main {public static void main(String[] args) throws Exception {for(int i=0; i<10; i++){new Man(i+"姓名", i, i+"性别");/* * 进行强制垃圾回收,以下语句等效于Runtime.getRuntime().gc(); */System.gc();}}}

        输出结果每一次都不一定相同,随机抽一次的输出结果如下:

/* * 我被gc调用了1次! * 我被gc调用了2次! * 我被gc调用了3次! * 我被gc调用了4次! */
        结论:程序new10个无引用的匿名对象,每new一个无引用的对象都会“强制”系统回收内存,但是从程序多次执行的表现观察,调用finalize方法的次数基本上每次都不相同而且不一定等于10次或0次,所以得出结论:java强制回收不是真的强制,而是对jvm的一种“建议”,“建议”其现在进行GC操作,但是jvm什么时候真正开始回收内存,对程序、程序员而言是透明的,不可预测。

  • finalize

        在垃圾回收机制回收某个对象所占用的内存之前,通常会要求调用适当的方法来清理资源,如果没有明确的指定,则java提供了默认机制来清理该对象所占用的内存,这个机制就是finalize方法,默认的finalize方法在java.lang.Object中被定义,可以被任何类重写。当一个对象从可达状态变成可恢复状态的时候,垃圾回收机制就标记这片内存可回收,但在真正回收内存之前会调用该对象的finalize方法来决定是否收回内存,但是是否调用了finalize方法以及何时调用finalize方法对程序员来说是透明的,由虚拟机来决定,一般情况下,只有当虚拟机认为内存紧张需要释放更多的堆内存的时候,垃圾回收机制才会进行垃圾回收。

        finalize:清理资源的默认机制,但该方法是否被调用以及何时被调用不可预测,由虚拟机决定。

        1. 最好不要主动调用对象的finalize方法,应该交给垃圾回收机制来调用;

        2. finalize方法不是一定会被执行的,并且当执行该方法出现异常,垃圾回收机制不会报告异常,虚拟机也不会感到异常,但finalize方法中异常后的代码不会被执行。

         将以上程序Man类的finalize方法稍作修改,程序如下,你会发现抛出异常之后的程序不会被执行,但是系统不会收到任何的异常欣喜

<span style="font-family:Microsoft YaHei;">        @Overrideprotected void finalize() throws Throwable {// TODO Auto-generated method stub/* * 这里将抛出空指针异常 */String str = null;System.out.println(str.length());/* * 异常之后,程序将不会继续执行,但是系统也不会收到异常信息 */count++;System.out.println("我被gc调用了"+count+"次!");}</span>

  • java中的引用

        为了更好的让java GC服务与我们,java语言提供了对对象的四种引用方式,更具引用的强弱顺序分别如下。

        1. 强引用:对象有一个及其以上的引用,例如Object oo = new Object();

        2. 软引用:java中通过SoftReference类来实现引用级别低于强引用当系统内存足够时起不会被系统回收当系统内存不够使其可能被回收

        3. 弱引用:通过WeakReference类实现,引用级别低于软引用,无论系统内存是否足够,系统进行垃圾回收的时候总是会回收其占用的内存;

        4. 虚引用:通过PhantomReference类实现引用级别最低类似没有引用主要用于跟踪对象被垃圾回收的状态

        需要重点关注的是,如果采用了除强引用之外的引用,那就一定要切断原对象的引用,否则引用不会达到预期的效果,以弱引用为例,引用的用法示例代码如下。

<span style="white-space:pre"></span>public static void main(String[] args) {/* * 1. 建立引用对象str * 2. 让str对象绑定弱引用 * 3. 切断str对象原来的强引用 * 效果:当系统进行垃圾回收的时候,无论内存是否足够,都会回收str所占用的内存 *       如果没有弱引用,则垃圾回收的时候不一定回收str所占用的资源 */String str = new String("弱引用示例");WeakReference<String> wr = new WeakReference<String>(str);str = null;}


  • 常用对象的判死算法

        1. 引用计数法:通过引用计数器来判断对象是否被引用,例如有一个引用则引用计数器+1,一般情况下引用计数法还是非常靠谱的判死算法,但是也有一个致命的缺点——对象的相互引用会导致无法回收内存。

        如以下代码所示,在A类中引用了B类对象,在B类中引用了A类对象,造成一个回环,导致引用计数法无法回收内存。

package demo;public class A {/* * 在A类中引用了B类对象 */B b = new B();}
package demo;public class B {/* * 在B类中引用了A类对象 */A a = new A();}
        因此,当AB两类实例化之后,即便断开引用,但是AB两类中的成员变量ab也会在堆内存中互相引用,导致程序计数器结果不为0,从而无法回收垃圾,图示如下。

        2. 可达性分析:类似树状结构,从“GC Root节点开始通过引用链搜索如果一个对象无路可达则表示该对象处于可恢复状态等待被GC回收内存资源。

  • 垃圾收集算法

        1. 标记-清除算法:先标记“不可达”状态的对象,然后统一回收内存资源;效率不高,同时会导致内存碎片化;

        2. 复制算法:为了提高“标记-清楚”算法的效率,将可用内存划分成大小相等的两块,每次只是用其中的一块,当这一块内存用完了(包含存活对象、等待回收的对象和碎片占用的空间),则一次性拷贝到另一块,此时将会释放等待回收对象的内存空间,同时解决内存碎片问题,存活对象连续占用一片内存,再一次性清空原先的内存块;

        3. 标记-整理算法:相比较“复制算法”,不会浪费50%的内存,算法不直接回收不可达对象占用的内存,而是让存活对象都向一端移动,然后清理释放边界的内存空间;

        4. 分代收集算法:根据对象的存活周期在堆中将对象分为新生代和老年代,根据不同代特点不同选择不同的算法,新生代——绝大部分对象很快不可达需要回收内存,采用复制算法,每次只需要付出极少的复制代价;老年代——绝大部分对象长期保持可达状态,适合采用“标记-清理”或“标记-整理”算法。

三、补充

        1. 随着操作系统的发展,32bit系统必然会逐渐被64bit系统取代虽然java虚拟机在很早以前就已经推出了64bit的版本但是直到目前,仍旧存在问题——java程序跑在64bit系统上系能消耗比32bit系统的多多消耗20%左右内存存在10%左右的性能差距;

        2. 首先要注意,java GC只针对内存的回收,对类似数据库连接、io等物力资源的释放不会自动回收,均需要手动close掉;

        3. 程序员无法精确的控制何时回收内存,只能给JVM“提供”建议或在代码上做一定的优化。


附注:

        本文如有错漏之处,烦请不吝指正,谢谢!


0 0