首先我们要明白一点,我们所使用的变量就是一块一块的内存空间!!
一、内存管理原理:
在java中,有java程序、虚拟机、操作系统三个层次,其中java程序与虚拟机交互,而虚拟机与操作系统间交互!这就保证了java程序的平台无关性!下面我们从程序运行前,程序运行中、程序运行内存溢出三个阶段来说一下内存管理原理!
1、程序运行前:JVM向操作系统请求一定的内存空间,称为初始内存空间!程序执行过程中所需的内存都是由java虚拟机从这片内存空间中划分的。
2、程序运行中:java程序一直向java虚拟机申请内存,当程序所需要的内存空间超出初始内存空间时,java虚拟机会再次向操作系统申请更多的内存供程序使用!
3、内存溢出:程序接着运行,当java虚拟机已申请的内存达到了规定的最大内存空间,但程序还需要更多的内存,这时会出现内存溢出的错误!
至此可以看出,Java 程序所使用的内存是由 Java 虚拟机进行管理、分配的。Java 虚拟机规定了 Java 程序的初始内存空间和最大内存空间,开发者只需要关心 Java 虚拟机是如何管理内存空间的,而不用关心某一种操作系统是如何管理内存的。
二、 RUNTIME 类的使用:
Java 给我们提供了Runtime 类得到JVM 内存的信息
方法名称 |
参数 |
作用 |
返回值 |
getRuntime |
无 |
获取Runtime 对象 |
Runtime 对象 |
totalMemory |
无 |
获取JVM 分配给程序的内存数量 |
long:内存数量 |
freeMemory |
无 |
获取当前可用的内存数量 |
long:内存数量 |
maxMemory |
无 |
获取JVM 可以申请到的最大内存数量 |
long:内存数量 |
三、内存空间逻辑划分:
JVM 会把申请的内存从逻辑上划分为三个区域,即:方法区、堆与栈。
方法区:方法区默认最大容量为64M,Java虚拟机会将加载的java类存入方法区,保存类的结构(属性与方法),类静态成员等内容。
堆:默认最大容量为64M,堆存放对象持有的数据,同时保持对原类的引用。可以简单的理解为对象属性的值保存在堆中,对象调用的方法保存在方法区。
栈:栈默认最大容量为1M,在程序运行时,每当遇到方法调用时,Java虚拟机就会在栈中划分一块内存称为栈帧(Stack frame),栈帧中的内存供局部变量(包括基本类型与引用类型)使用,当方法调用结束后,Java虚拟机会收回此栈帧占用的内存。
四、java数据类型
声明此类型变量,只会在栈中分配一块内存空间。
2、引用类型:就是底层封装指针的数据类型。
他们在内存中分配两块空间,第一块内存分配在栈中,只存放别的内存地址,不存放具体数值,我们也把它叫指针类型的变量,第二块内存分配在堆中,存放的是具体数值,如对象属性值等。
3、下面我们从一个例子来看一看:
public class Student {
String stuId;
String stuName;
int stuAge;
}
public class TestStudent {
public static void main(String[] args) {
Student zhouxingxing = new Student();
String name = new String("旺旺");
int a = 10;
char b = 'm';
zhouxingxing.stuId = "9527";
zhouxingxing.stuName = "周星星";
zhouxingxing.stuAge = 25;
}
}
(1)类当然是存放在方法区里面的。
(2)
Student zhouxingxing = new Student();
这行代码就创建了两块内存空间,第一个在栈中,名字叫zhouxingxing,它就相当于指针类型的变量,我们看到它并不存放学生的姓名、年龄等具体的数值,而是存放堆中第二块内存的地址,第二块才存放具体的数值,如学生的编号、姓名、年龄等信息。
(3)
int a = 10;
这是
基本数据类型 变量,具体的值就存放在栈中,并没有只指针的概念!
下图就是本例的内存布置图:
此外我们还要知道
Student zhouxingxing = new Student(); 包括了声明和创建,即:
Student zhouxingxing;和
zhouxingxing = new Student();其中声明只是在栈中声明一个空间,但还没有具体的值,声明后的情况如下图所示:
(4)
引用类型中的数组也封装了指针,即便是基本数据类型的数组也封装了指针,数组也是引用类型。比如代码int[] arr = new int[]{23,2,4,3,1};如下图所示:
五、java值传参与引用参数
(1)参数根据调用后的效果不同,即是否改变参数的原始数值,又可以分为两种:按值传递的参数与按引用传递的参数。
按值传递的参数原始数值不改变,按引用传递的参数原始数值改变!这是为什么呢?其实相当简单:
我们知道基本数据类型的变量存放在栈里面,变量名处存放的就是变量的值,那么当基本数据类型的变量作为参数时,传递的就是这个值,只是把变量的值传递了过去,不管对这个值如何操作,都不会改变变量的原始值。而对引用数据类型的变量来说,变量名处存放的地址,所以引用数据类型的变量作为传参时,传递的实际上是地址,对地址处的内容进行操作,当然会改变变量的值了!
(2)特例:string
public class TestString {
public static void main(String[] args) {
String name = "wangwang";
TestString testString = new TestString();
System.out.println("方法调用前:" + name);
testString.change(name);
System.out.println("方法调用后:" + name);
}
void change(String str) {
str = "旺旺老师";
System.out.println("方法体内修改值后:" + str);
}
}
结果:
方法调用前:wangwang
方法体内修改值后:旺旺老师
方法调用后:wangwang
分析:
上例中,虽然参数String 是引用数据类型,但其值没有发生改变,这是因为String 类
是final 的,它是定长,我们看初始情况,即String name = "wangwang";这行代码运行
完,如下图:
当调用方法时testString.change(name),内存变化为:
在方法体内,参数str赋予一个新值,str = "旺旺老师"。因为String是定长,系统就会在堆中分配一块新的内存空间37DF,这样str指向了新的内存空间37DF,而name还是指向36DF, 37DF的改变对它已没影响:
最后,方法调用结束,str与37DF的内存空间消亡。Name的值依然为wangwang,并没有改变。
所以String虽然是引用类型参数,但值依然不变:
(3)无法交换的例子:
public class TestChange {
void change(Student stu1, Student stu2) {
stu1.stuAge ++;
stu2.stuAge ++;
Student stu = stu1;
stu1 = stu2;
stu2 = stu;
}
public static void main(String[] args) {
Student furong = new Student();
furong.stuName = "芙蓉姐姐";
furong.stuAge = 30;
Student fengjie = new Student();
fengjie.stuName = "凤姐";
fengjie.stuAge = 26;
TestChange testChange = new TestChange();
testChange.change(furong, fengjie);
System.out.println(furong.stuName);
System.out.println(furong.stuAge);
System.out.println(fengjie.stuName);
System.out.println(fengjie.stuAge);
}
}
分析:
内存泄漏:
java
内存管理中的内存泄漏产生的主要原因:保留下来却永远不再使用的对象引用。
导致内存泄漏主要的原因是,先前申请了内存空间而忘记了释放。如果程序中存在对无用对象的引用,那么这些对象就会驻留内存,消耗内存,因为无法让垃圾回收器GC验证这些对象是否不再需要。如果存在对象的引用,这个对象就被定义为"有效的活动",同时不会被释放。要确定对象所占内存将被回收,我们就要务必确认该对象不再会被使用。典型的做法就是把对象数据成员设为null或者从集合中移除该对象。但当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是有被引用的,即在有向树形图中,存在树枝通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
比如说一个静态的map(静态变量从所在内装载的时候起,一直到程序结束都有效),加入了很多名值对,删除某个key值对应的value的时候计算key值错误,导致以为删除了,但是实际没有删除,这样对应的value会一直不会被回收
3.3 容易引起内存泄漏的几大原因
3.3.1 静态集合类
像HashMap、Vector 等静态集合类的使用最容易引起内存泄漏,因为这些静态变量的生命周期与应用程序一致,如示例1,如果该Vector 是静态的,那么它将一直存在,而其中所有的Object对象也不能被释放,因为它们也将一直被该Vector 引用着。
3.3.2 监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
3.3.3 物理连接
一些物理连接,比如数据库连接和网络连接,除非其显式的关闭了连接,否则是不会自动被GC 回收的。Java 数据库连接一般用DataSource.getConnection()来创建,当不再使用时必须用Close()方法来释放,因为这些连接是独立于JVM的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。
3.3.4 内部类和外部模块等的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。对于程序员而言,自己的程序很清楚,如果发现内存泄漏,自己对这些对象的引用可以很快定位并解决,但是现在的应用软件并非一个人实现,模块化的思想在现代软件中非常明显,所以程序员要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
public void registerMsg(Object b);
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
4 预防和检测内存漏洞
在了解了引起内存泄漏的一些原因后,应该尽可能地避免和发现内存泄漏。
(1)好的编码习惯。最基本的建议就是尽早释放无用对象的引用,大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域后,自动设置为null。在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组、列、树、图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null。
另外建议几点:
在确认一个对象无用后,将其所有引用显式的置为null;
当类从Jpanel 或Jdialog 或其它容器类继承的时候,删除该对象之前不妨调用它的removeall()方法;
在设一个引用变量为null 值之前,应注意该引用变量指向的对象是否被监听,若有,要首先除去监听器,然后才可以赋空值;
当对象是一个Thread 的时候,删除该对象之前不妨调用它的interrupt()方法;
内存检测过程中不仅要关注自己编写的类对象,同时也要关注一些基本类型的对象,例如:int[]、String、char[]等等;
如果有数据库连接,使用try...finally 结构,在finally 中关闭Statement 对象和连接。
(2)好的测试工具。在开发中不能完全避免内存泄漏,关键要在发现有内存泄漏的时候能用好的测试工具迅速定位问题的所在。市场上已有几种专业检查Java 内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java 程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler、JProbe Profiler、JinSight、Rational 公司的Purify 等。