java 虚拟机内存模型

来源:互联网 发布:充值话费分销源码 编辑:程序博客网 时间:2024/05/18 00:02

历史:
        CPU对磁盘的读写速度,远远跟不上 CPU 的计算速度,为了解决这个问题,所以引入了内存;
        随着 CPU 的发展,内存的读写速度也跟不上CPU的计算速度,为了解决这个问题,硬件厂商又在每个cpu上增加了高速缓存,所以现在 CPU 和内存的交互就变成:
                                                                                   图1
       
基于高速缓存的存储体系解决了处理器与内存之间的矛盾,同时也引入了新的问题:缓存一致性问题
在多处理器系统中,每个处理器有自己的高速缓存,同时他们又共享同一块内存,当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象:

                         
                                       图2
java 虚拟机内存:
Java 虚拟机可以看作是一台抽象的计算机,自己的指令集以及 各种运行时内存区域,内存模型中定义的访问操作与物理计算机处理的基本一致: 
                                          图3
这种存储结构带来的就是线程间的数据同步问题,比如单例实现方法:
class SingletonDemo {
    private static SingletonDemo sInstance;

    private SingletonDemo() {
    }

    public SingletonDemo getInstance() {
        if (sInstance == null) {
            sInstance new SingletonDemo();
        }
        return sInstance;
    }
}

为了解决这些问题,java 引入了 volatile  同步锁 等机制用来解决多线程的同步问题:
class SingletonDemo {
    private volatile static SingletonDemo sInstance;

    private SingletonDemo() {
    }

    public SingletonDemo getInstance() {
        if (sInstance == null) {
            synchronized (SingletonDemo.class) {
                if (sInstance == null) {
                    sInstance new SingletonDemo();
                }
            }
        }
        return sInstance;
    }
}

java虚拟机内存模型从功能逻辑角度又可以划分为如下模型:

                         
                                                                                                    图4
1、程序计数器
     程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
     由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
     此区域是唯一没有规定任何out of memoryError 的区域;
2、Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame):
java.lang.ClassCastException: com.gionee.client.GNSplashActivity cannot be cast to com.gionee.client.activity.base.BaseFragmentActivity
08-07 15:32:18.656 4650 4650 W System.err: at com.gionee.framework.operation.net.NetRequestHandler$MyRunable$1.run(NetRequestHandler.java:105)
08-07 15:32:18.656 4650 4650 W System.err: at android.os.Handler.handleCallback(Handler.java:836)
08-07 15:32:18.656 4650 4650 W System.err: at android.os.Handler.dispatchMessage(Handler.java:103)
08-07 15:32:18.656 4650 4650 W System.err: at android.os.Looper.loop(Looper.java:203)
08-07 15:32:18.657 4650 4650 W System.err: at android.app.ActivityThread.main(ActivityThread.java:6346)
08-07 15:32:18.657 4650 4650 W System.err: at java.lang.reflect.Method.invoke(Native Method)
08-07 15:32:18.657 4650 4650 W System.err: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1111)
08-07 15:32:18.657 4650 4650 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:972)

异常:
     如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一 个 StackOverflowError 异常;
   
                                        
  
                                                                                                
如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚 拟机将会抛出一个 OutOfMemoryError 异常;
                                 

                    
                                                                                 
3、本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。
4、Java 堆
对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。java 堆是垃圾收集器管理的主要区域,所以也被称为是"GC 堆"

                                                
                                                                                                       
5、方法区
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。
                                     

这段代码是同步锁对象是存放在方法区的对象:

private static final String DEFAULT_VALUE "";
/**
 * 获取手机IMEI(未加密)
 * 
 * @param context
 @return
 */
public static String getIMEI(final Context context) {
    if (!DEFAULT_VALUE.equals(sIMEI)) {
        return sIMEI;
    }
    synchronized (DEFAULT_VALUE) {
        if (!DEFAULT_VALUE.equals(sIMEI)) {
            return sIMEI;
        }
        for (String prop : sPropList) {
            String value = getSystemProp(prop);
            if (TextUtils.isEmpty(value)) {
                continue;
            }
            sIMEI = value;
            return sIMEI;
        }
        String deviceId = getDeviceId(context);
        if (TextUtils.isEmpty(deviceId)) {
            return DEFAULT_IMEI;
        }
        sIMEI = deviceId;
        return sIMEI;
    }
}

                     

自动内存回收(garbage collection):
java 与 c++之间有一堵由内存动态分配和垃圾收集技术围成的高墙,墙外的人想进去,墙内的人想出来.

搞清楚三个问题:
(1) 什么时候启动内存回收?
(2) 对什么对象做回收?
(3) 内存回收是如何执行的?

什么时候启动内存回收?
     垃圾收集的主要对象是堆内存,Java堆内存分为年轻代年代:
年轻代(new generation):年轻代用来存放JVM刚分配的Java对象
年代(old generation):年轻代中经过垃圾回收没有回收掉的对象将被Copy到老
年轻代又分为三个部分:
Eden(伊甸园):Eden用来存放JVM刚分配的对象
Survivor1(幸存者1):
Survivro2(幸存者2):两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到年代
gc就是在两个不同的区域中发生:
Minor GC:从年轻代回收内存
Full GC:是清理整个堆空间包括年轻代和老年代

1、所有新生成的对象都是放在年轻代的Eden分区的,初始状态下两个Survivor分区都是空的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
                                                     


2、当Eden区满的的时候,Minor GC就会被触发。
                                                       

3、当Eden分区进行清理的时候,会把引用对象移动到第一个Survivor分区,无引用的对象删除。

                                                         


4、在下一个Minor GC的时候,在Eden分区中会发生同样的事情:无引用的对象被删除,引用对象被移动到另外一个Survivor分区(S1)。此外,从上次小垃圾收集过程中第一个Survivor分区(S0)移动过来的对象年龄增加,然后被移动到S1。当所有的幸存对象移动到S1以后,S0和Eden区都会被清理。注意到,此时的Survivor分区存储有不同年龄的对象。
                                                           


5、再下一个Minor GC同样的过程反复进行。此时Survivor分区的角色发生了互换,引用对象被移动到S0,幸存对象年龄增大。Eden和S1被清理。
                                                            


6、这幅图展示了从年轻代到老年代的提升。当进行一个Minor GC之后,如果此时年老对象此时到达了某一个个年龄阈值(例子中使用的是8),JVM会把他们从年轻代提升到老年代。
                                                           


7、随着Minor GC持续进行,对象将会被持续提升到老年代。
                                                        


8、这样几乎涵盖了年轻一代的整个过程。最终,在老年代将会进行Full GC,这种收集方式会清理-压缩老年代空间。
                                                        





     Minor GC发生时间:   对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快;
     Full GC发生时间:当老年代没有足够的空间时即发生Full GC,发生Full GC; 发生Minor GC前,虚拟机会检测老年代的剩余空间大小是否大于新生代所有对象的总空间,如果大于,那么这次Minor GC 是安全的,如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,继续检查老年代的剩余空间大小是否大于历次晋升到老年代对象的平均大小,如果大于那只会进行一次Minor GC,如果小于或者是HandlePromotionFailure设置不允许,则改为进行一次Full GC。

对什么对象做回收?
      垃圾收集器在对堆进行回收前,首先需要确定哪些对象还"活着",哪些已经"死亡",活着的对象不能回收,死亡的对象列入回收参考范围。
可达性分析算法
 
                
                                                                                       图7     
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
  (1) 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  (2) 方法区中类静态属性引用的对象。
  (3) 方法区中常量引用的对象。
  (4)  本地方法栈中JNI(即一般说的Native方法)引用的对象。
      对象生存还是死亡(To Die Or Not To Die) 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finanize()方法。当对象没有覆盖finanize()方法,或者finanize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
                             
public class FinalizeDemo {
    public static FinalizeDemo SAVE_HOOK null;

    public void isApve() {
        System.out.println("yes, i am still apve :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeDemo.SAVE_HOOK this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK new FinalizeDemo();

        //象第一次成功拯救自己
        SAVE_HOOK null;
        System.gc();
        //finapze方法很低所以0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isApve();
        } else {
            System.out.println("no, i am dead");
        }

        //下面段代与上面的完全相同但是次自救却失

        SAVE_HOOK null;
        System.gc();
        // finalize方法很低所以0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isApve();
        } else {
            System.out.println("no, i am dead");
        }
    }

}


finalize mehtod executed!
yes, i am still apve
no, i am dead 
 内存回收执行方法:
jvm内存回收有三种基本算法
(1) 标记-清除
标记清除算法最简单,主要是标记出来需要回收的对象,然后然后把这些对象在内存的信息清除。
                                    2_1

(2)标记-清除-压缩   这个算法是在标记-清除的算法之上进行一下压缩空间,重新移动对象的过程。因为标记清除算法会导致很多的留下来的内存空间碎片,随着碎片的增多,严重影响内存读写的性能,所以在标记-清除之后,会对内存的碎片进行整理。最简单的整理就是把对象压缩到一边,留出另一边的空间。由于压缩空间需要一定的时间,会影响垃圾收集的时间。
                                      2_2


(3)标记-清除-复制 这个算法是把内存分配为两个空间,一个空间(A)用来负责装载正常的对象信息,,另外一个内存空间(B)是垃圾回收用的。每次把空间A中存活的对象全部复制到空间B里面,在一次性的把空间A删除。这个算法在效率上比标记-清除-压缩高,但是需要两块空间,对内存要求比较大,内存的利用率比较低。适用于短生存期的对象,持续复制长生存期的对象则导致效率降低
                                             2_3


不同的存储区域使用不同的收集算法。
新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,则使用(3)标记-清除-复制 ,新生代内存被分为一个较大的Eden区和两个较小的Survivor区,每次只使用Eden区和一个Survivor区,当回收时将Eden区和Survivor还存活着的对象一次性的拷贝到另一个Survivor区上,最后清理掉Eden区和刚才使用过的Survivor区;
老年代中对象一般存活时间比较长,使用(1) 标记-清除(2)标记-清除-压缩,每次标记清除之后,会有很多的零碎空间,当老年代的零碎空间不足以分配一个大的对象的时候,就会采用压缩算法。在压缩的时候,应用需要暂停,stop  the world.


常见内存泄露案例:
1  广播注册未注销;
2 添加观察者  未删除;
3 静态变量应用;
4 非静态内部类;
handler 引用用context
postDelay
5 webview的内存泄露:

ViewParent parent = mWebView.getParent();
if (parent != null) {
    ((ViewGroup) parent).removeView(mWebView);
}
mWebView.destroy();



参考书籍:
深入理解java虚拟机-------周志明
Java虚拟机规范(Java SE 7版)