一 Java中的内存分配
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
1、静态存储区(方法区):主要存放静态数据、全局static数据和常量这块区域在程序编译时就已经分配好,并且在程序的整个运行期间都存在;
2、栈存储区:当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3、堆存储区:又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
举个例子?
public class Apple{
int s1 = 0;
Apple mApple1 = new Apple();
public void method() {
int s2 = 1;
Apple mApple2 = new Apple();
}
}
Apple mApple3 = new Apple();
s2和mApple2 都是Apple类中的局部变量,所以存在于栈中,但mApple2指向的对象是存在于堆中的;
mApple3存在于栈中,但它指向的对象存在于堆中,包括这个对象的成员变量 s1和mApple1;
得出结论:
1、局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。
2、成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。
二、Java虚拟机内存模型
绿色模块:由所有线程共享的数据区
蓝色模块:线程隔离的数据区
方法区即上面我们讲到的静态存储区、堆即是我们上面提到的堆存储区;
1、虚拟机栈
它的生命周期与线程相同,虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧( Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常
如果虚拟机栈扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常
2、本地方法栈
与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native方法服务。
3、程序计数器
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
三、Java内存管理
内存管理就是内存分配和释放的问题,在Java中程序员通过new为每个对象申请内存空间,而对象的释放是由GC来决定和执行的,那么GC是如何来判断对象是否要回收的呢?
有向图
main是程序的入口即有向图的根点,obj对象是有向图的顶点,如果从根点到顶点可以连接起来说明该对象正在被引用,反之则为无引用对象,可以被回收;
四、Java中的内存泄漏
内存泄漏是指无用对象,持续占有内存,得不到及时释放从而造成内存空间的浪费;内存泄漏的根本原因就是,长生命周期的对象持有短生命周期对象的引用,导致短生命周期对象无法释放;
1、静态集合类引起的内存泄漏
HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++){
Object o = new Object();
v.add(o);
o = null;
}
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
2、监听器引起的内存泄漏
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
3、各种连接引起的内存泄漏
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
4、内部类和外部模块的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: public void registerMsg(Object obj); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
5、单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏;
以上简单的介绍了几种在Java中出现内存泄漏的场景,我们虽然不会写出这么低级的代码,但是也不可避免会疏忽,只要我们了解了Java的内存管理机制和内存泄漏的真正原因,因而就可以最大程度的去避免内存泄漏,写出高质量的代码;