关注「Java视界」公众号,获取更多技术干货

Java内存分析图解(内存分类、结合代码的内存分析、内存泄露)

 

(一)JVM的内存分类

首先JVM的内存分为栈内存、堆内存及方法区:

栈内存:

  • 连续的存储空间,遵循后进先出的原则
  • 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)
  • 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问
  • 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)

堆内存:

  • 不连续的空间,用于存放new出的对象,保存的是真正的数据
  • JVM只有一个堆区(heap)被所有线程共享

方法区:

  • 又叫静态区,跟堆一样,被所有的线程共享。
  • 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量等静态属性(①类的代码信息;②静态变量和方法;③常量池(字符串常量等,具有共享机制))。

总得来说,堆内存保存对象的真实信息,只要是new出来的对象都在堆内存;栈内存保存的是一块堆内存的地址,通过该地址可以找到对应的真实对象数据;方法区是保存静态属性。

(二)内存分析图解

1、示例1      

Ball ball = new Ball(); 
ball.name = "篮球";
ball.size = 12.6;

内存变化过程如下:

当然以上的“篮球”实际上是保存在方法区的。

 

2、示例2

Ball ball = null; 
ball = new Ball(); 
ball.name = "篮球";
ball.size = 12.6;

内存变化过程如下:

 

3、示例3(引用传递)

引用传递产生的原因是同一块堆内存可以被多个栈内存指向。

Ball ball = new Ball(); 
ball.name = "篮球";
ball.size = 12.6;
Ball ball2 = ball;
ball2.size = 66;

内存变化过程如下:

 

4、示例4(引用传递,垃圾产生)

Ball ball = new Ball(); 
Ball ball2 = new Ball(); 
ball.name = "篮球";
ball.size = 12.6;
ball2.name = "乒乓球";
ball2.size = 6.6;
ball2 = ball;
ball2.size = 6666;

内存变化过程如下:

上面,ball2原本指向的一块堆内存在引用传递后,再也没有指向它的引用,就会成为垃圾空间被JVM的GC回收掉。

一个栈内存只能保存一个堆内存的地址,如果发生改变,之前指向的堆内存就会成为垃圾空间。垃圾空间是指没有任何栈内存所指向的堆内存空间,由GC不定期回收,垃圾空间过多会影响GC的处理性能。

 

5、示例5(static属性及方法内存分析)

static修饰公共的属性及方法,其属性或方法是在方法区保存的。

 

(三)内存泄露

上面图解分析时,说明了垃圾空间的产生过程,垃圾空间和内存泄露不是一个概念,垃圾空间只是无引用指向它但还可以被GC回收再利用,内存泄露广义的来说是:不再会被使用的对象的内存不能被回收,就是内存泄露。

一些不再会被使用的对象,在GC看来不能被释放,就会造成内存泄露。到底啥时候会造成内存泄露呢?简单来说,就是当长生命周期的对象持有短生命周期的对象时,就会产生内存泄露。

比如:

public class Demo{
    
    Object object;
 
    public void someMethod(){
        object = new Object();
    //...
    }
}

以上,Demo类产生的就是长生命周期的对象,object就是短生命周期对象,这里我们其实是想要someMethod()执行完以后,object这个对象就消失的,因为只在这个方法中会用到object这个对象,但实际是object这个对象不会被释放继续占用堆内存,直到Demo这个类产生的对象无用以后才会被回收,object明明已经不用了,但是还占着堆内存,这个期间就产生了内存泄露。

解决这种内存泄露就需要把短生命周期的对象定义成局部变量,随着方法体的结束而结束。以上代码可改写为:

public class Demo{
    
    public void someMethod(){
        Object object = new Object();
    //...
    }
}

或者 手动标记成垃圾空间,由GC回收:

public class Demo{
    
    Object object;
 
    public void someMethod(){
        object = new Object();
    //...
        
        object = null; // 手动置为null,GC会认为无引用指向这块堆内存,自动回收
    }
}

因此,在内存对象明明已经不需要的时候,不要继续保留着这块内存和它的访问方式(引用),否则会产生内存泄露。

一些容易发生内存泄露的例子和解决方法

 像上面例子中的情况很容易发生,也是我们最容易忽略并引发内存泄露的情况,解决的原则就是尽量减小对象的作用域(比如android studio中,上面的代码就会发出警告,并给出的建议是将类的成员变量改写为方法内的局部变量)以及手动设置null值。

 单例模式导致的内存泄露

    单例模式,很多时候我们可以把它的生命周期与整个程序的生命周期看做差不多的,所以是一个长生命周期的对象。如果这个对象持有其他对象的引用,也很容易发生内存泄露。

在Activity中创建了一个SingleInstance,并且将Activity的实例this传递给了该类的对象,导致该单例对象持有了对应的Activity的引用。当我们Activity退出后,由于SingleInstance还存在,它的生命周期并没有结束,所以SingleInstance依然持有对Activity实例的引用,由于Activity有被引用,导致Activity的实例不能被回收,Activity会长时间的存在内存中。

解决方案:使用弱引用。

  弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2022-06-25 14:03  沙滩de流沙  阅读(413)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货