从java内存分配角度分析android内存泄漏问题

来源:互联网 发布:淘宝网玩具批发市场 编辑:程序博客网 时间:2024/05/14 09:39

转载请注明出处:http://blog.csdn.net/u011510784/article/details/51691584

在android程序中,因使用单例模式而带来的内存泄漏问题比如下面这种:

<span style="font-size:18px;">public class Test {    private static Test mInstance;    private Context context;    private Test(Context context) {        this.context = context;    }    public static Test getInstance(Context context) {        if (mInstance == null)            mInstance = new Test(context);        return mInstance;    }    public void showMessage(String message) {        if (message == null) message = "no message";        Toast.makeText(context, message, Toast.LENGTH_LONG).show();    }}</span>


简单的测试代码,先不管它的实际使用价值。

这里的Test类会一直持有传入的这个context的引用,如果传入的context是个activity,那么这个activity就会内存泄漏,如果上面的代码这样写呢:

<span style="font-size:18px;">public class Test {    private static Test mInstance;    private Test() {    }    public static Test getInstance(Context context) {        if (mInstance == null)            mInstance = new Test();        return mInstance;    }    public void showMessage(Context context, String message) {        if (message == null) message = "no message";        Toast.makeText(context, message, Toast.LENGTH_LONG).show();    }}</span>


如果传入的context是个activity,这个activity也会内存泄漏吗? 我们先分析一下Java 的内存分配规则:

JVM的内存主要分为五个部分

1、Heap (堆):一个Java虚拟实例中只存在一个堆空间,heap (堆)是JVM的内存数据区。heap 的管理就比较复杂了,每次分配不定长的内存空间,专门用来保存对象的实例。在heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在stack中),在heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例在heap 中分配好以后,需要在stack中保存一个4字节的heap 内存地址,用来定位该对象实例在heap 中的位置,便于找到该对象实例。

        按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给 自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。


2、Method Area(方法区域):被装载的class的信息存储在Method area的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,

然后读入这个class文件内容并把它传输到虚拟机中。


3、Java Stack(java的栈):stack(栈)是JVM的内存指令区。stack的速度很快,管理很简单,并且每次操作的数据或者指令字节长度是已知的。Java 基本数据类型,Java 指令代码,常量都保存在stack中。虚拟机只会直接对Java stack执行两种操作:以帧为单位的压栈或出栈。


4、Program Counter(程序计数器):每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的

饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。  

5、Native method stack(本地方法栈):保存native方法进入区域的地址


以上五部分只有Heap 和Method Area是被所有线程的共享使用的;而Java stack, Program counter 和Native method stack是以线程为粒度的,每个线程独自拥有自己的部分。

所以类中的方法,以及方法中的局部变量是分配在栈中的,当方法执行结束后,栈会释放掉这部分内存。

由于stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而heap则是随机分配内存,不定长度,存在内存分配和回收的问题;因此在JVM中另有一个GC进程,定期扫描heap ,它根据stack中保存的4字节对象地址扫描heap,定位heap中这些对象,把stack中丢失了对象地址的无用对象清除掉,这就是垃圾收集的过程。

当创建一个类的实例时,这个类中的成员变量,成员方法,以及方法中的局部变量都保存到哪里呢?

   举个例子:

<span style="font-size:18px;">public class Sample() {    int s1 = 0;    Sample mSample1 = new Sample();    public void method() {        int s2 = 1;        Sample mSample2 = new Sample();    }}Sample mSample3 = new Sample();</span>


Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。
mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。


具体一些:

基础数据类型直接在栈空间分配;
方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;
引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;
局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收;


即:当类中的方法被调用时,Java虚拟机会在栈中创建一个方法帧(frame),退出该方法则对应的方法帧被弹出(pop)。

方法本身是指令的操作码部分,保存在stack中;
方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在stack中(实际上是简单类型保存在stack中,对象类型在stack中保存地址,在heap 中保存值);
上述的指令操作码和指令操作数构成了完整的Java 指令。
对象实例包括其属性值作为数据,保存在数据区heap 中。
非静态的对象属性作为对象实例的一部分保存在heap 中,而对象实例必须通过stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在stack中的地址指针。


那么静态方法和非静态方法在内存中的情况是怎么样的呢?

非静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,这个隐含的参数就是对象实例在stack中的地址指针。因此非静态方法(在stack中的指令代码)总是可以找到自己的专用数据(在heap 中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。
而静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的stack,该静态方法即可被调用。当然此时静态方法是存取不到heap 中的对象属性的。


静态属性和动态属性:
前面提到对象实例以及动态属性都是保存在heap 中的,而heap 必须通过stack中的地址指针才能够被指令(类的方法)访问到。因此可以推断出:静态属性是保存在stack中的(基本类型保存在stack中,对象类型地址保存在stack,值保存在heap中),而不同于动态属性保存在heap 中。正因为都是在stack中,而stack中指令和数据都是定长的,因此很容易算出偏移量,也因此不管什么指令(类的方法),都可以访问到类的静态属性。也正因为静态属性被保存在stack中,所以具有了全局属性。


通过上面的分析,是不是可以这样说:

单例模式的Test类在实例化的时候,静态的引用变量 private static Test mInstance 存放在静态存储区,(指针存在栈中,实例存在堆中?),当调用showMessage()方法时,在栈中创建这个方法帧,方法中传进来的参数context的指针存在栈中,当方法结束时,在栈中所分配的相应的内存也会被回收。

所以,后面那种写法不会造成内测泄露?文中若有错误,还请大家指出。



0 0
原创粉丝点击