Java内存管理

来源:互联网 发布:表格如何筛选重复数据 编辑:程序博客网 时间:2024/05/18 20:12

=======================背景知识===============================

作为Internet最流行的编程语言之一,Java现正非常流行。我们的网络应用程序就主要采用Java语言开发,大体上分为客户端、服务器和数据库三个层次。在进入测试过程中,我们发现有一个程序模块系统内存和CPU资源消耗急剧增加,持续增长到出现java.lang.OutOfMemoryError为止。经过分析Java内存泄漏是破坏系统的主要因素。这里与大家分享我们在开发过程中遇到的Java内存泄漏的检测和处理解决过程.

本文先介绍Java的内存管理,以及导致Java内存泄露的原因。

一. Java是如何管理内存

为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但它只能回收无用并且不再被其它对象引用的那些对象所占用的空间。

Java的内存垃圾回收机制是从程序的主要运行对象开始检查引用链,当遍历一遍后发现没有被引用的孤立对象就作为垃圾回收。GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

在Java中,这些无用的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。虽然,我们有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC。通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。

二. 什么是Java中的内存泄露

导致内存泄漏主要的原因是,先前申请了内存空间而忘记了释放。如果程序中存在对无用对象的引用,那么这些对象就会驻留内存,消耗内存,因为无法让垃圾回收器GC验证这些对象是否不再需要。如果存在对象的引用,这个对象就被定义为"有效的活动",同时不会被释放。要确定对象所占内存将被回收,我们就要务必确认该对象不再会被使用。典型的做法就是把对象数据成员设为null或者从集合中移除该对象。但当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是有被引用的,即在有向树形图中,存在树枝通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

这里引用一个常看到的例子,在下面的代码中,循环申请Object对象,并将所申请的对象放入一个Vector中,如果仅仅释放对象本身,但因为Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。

  1. Vector v = new Vector(10);      
  2. for (int i = 1; i < 100; i++)      
  3. {      
  4.  Object o = new Object();      
  5.  v.add(o);      
  6.  o = null;      
  7. }//此时,所有的Object对象都没有被释放,因为变量v引用这些对象。     

实际上这些对象已经是无用的,但还被引用,GC就无能为力了(事实上GC认为它还有用),这一点是导致内存泄漏最重要的原因。 再引用另一个例子来说明Java的内存泄漏。假设有一个日志类Logger,其提供一个静态的log(String msg),任何其它类都可以调用Logger.Log(message)来将message的内容记录到系统的日志文件中。

Logger类有一个类型为HashMap的静态变量temp,每次在执行log(message)的时候,都首先将message的值写入temp中(以当前线程+当前时间为键),在退出之前再从temp中将以当前线程和当前时间为键的条目删除。注意,这里当前时间是不断变化的,所以log在退出之前执行删除条目的操作并不能删除执行之初写入的条目。这样,任何一个作为参数传给log的字符串最终由于被Logger的静态变量temp引用,而无法得到回收,这种对象保持就是我们所说的Java内存泄漏。 总的来说,内存管理中的内存泄漏产生的主要原因:保留下来却永远不再使用的对象引用



=============================细节=====================================


首先我们要明白一点,我们所使用的变量就是一块一块的内存空间!!

一、内存管理原理:

在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数据类型




1、基本数据类型:没封装指针的变量
声明此类型变量,只会在栈中分配一块内存空间

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);
  }
}

运行结果:
芙蓉姐姐
31
凤姐
27

分析:



原创粉丝点击