一 Java内存机制概述
作者是先学习C++,后来才学习的Java。在编写程序的时候,感觉二者最不一样的地方就是:在Java中,程序员并不需要对动态开辟的存储空间(new创建的对象或数组)进行回收(delete操作)。这些工作将会由JVM的垃圾自动回收器接手。Java的内存管理分为以下几个部分:栈(Stack),堆(Heap),方法区(Method Area)。下面几节将会分别介绍这几个区域。
二 栈
栈中保存的是Java八种基本数据类型的变量(short, int, long, byte, double, float, char, boolean),对象的引用变量(相当于对象实例或者数组的名字,也就是Java中的指针),方法帧。当程序员在代码中定义了上述类型的变量时,JVM就会在栈内存中为这些变量分配空间。
需要注意的是:1.引用变量保存的是对象的实例或数组在堆内存中的地址。2.栈内存是线程私有的。JVM为每一个线程分配一个栈,这个栈的生命周期与线程相同。3.当调用程序中的一个方法时,JVM就会吧方法帧(方法需要的参数,方法的局部变量,方法的中间结果)压入栈,当使用结束时,把方法帧弹出栈(同时自动释放掉这些变量被分配的空间)。
三 堆
程序员使用new创建对象和数组所分配的内存空间,称为堆内存。换句话来说,堆内存中存放的是对象的实例和数组。在Java中,程序员并不需要像在C++中那样手动回收这些内存。与栈不同,在多线程开发中,堆内的数据是所有线程栈共享的(并非为每一个线程开辟出一块堆内存)。
需要注意的是:当对象或者数组的引用变量在其生命周期结束时被销毁,但存放在堆内存中的对象的实例和数组并不会被立刻销毁,JVM的内存回收器(Garbage Collection)会在某个不确定的时间回收这部分内存。
例子1:对象的创建
class Student {
String name;
int grade;
public Student(String name, int grade){
this.name = name;
this.grade = grade;
}
}
public class HelloWorld{
public static void main(String[] args){
Student s1 = new Student("caoxi",4);
Student s2 = new Student("quyuan",6);
System.out.println("If s1 equals s2: " + (s1 == s2));
}
}
程序的输出结果:If S1 equals S2: false
说明S1和S2保存的是不同的堆内存地址,这两个内存地址分别指向的是两个不同的对象实例。
例子2:引用的传递
class Student {
String name;
int grade;
public Student(String name, int grade){
this.name = name;
this.grade = grade;
}
}
public class HelloWorld{
public static void main(String[] args){
Student s1 = new Student("caoxi",4);
Student s2 = new Student("quyuan",6);
s2 = s1;
System.out.println("If s1 equals s2: " + (s1 == s2));
}
}
由上面这个例子可以看到。引用变量与对象并非是必须是一一对应的。引用传递就是讲一个堆内存空间的使用权分配给多个栈内存空间,也就是说可以有多个引用变量指向同一个对象的实例(此时这两个引用变量存储的堆内存地址是相同的)。例子2中,由于引用变量S2之前已经指向了一个堆内存空间,在引用传递时,它将切断之前的链接而指向对一个对象实例(删去第二个对象实例的地址,存储第一个对象实例的地址),此时第二个对象实例没有任何引用,变成了垃圾,它将等待JVM的垃圾回收器在某个不确定的时间回收掉。
在软件设计中,我们将栈内存中的存储内容看作是程序的逻辑,而把堆内存中存储的内容看作是程序的数据。关于栈和堆的访问速度,访问栈的速度要略快于访问堆的速度,尽管二者都是内存,在物理上的访问机制相同,但访问方式并不相同。
1. 栈内存相较于堆内存来说通常要小很多(这也是满足了为每个线程开辟一个栈的条件),在建立了cache和内存映射的条件下(上述二者只能在有限内存块大小内进行),根据局部性的关系,命中率高,所以访问速度要快很多。
2.访问堆内存中的内容通常需要两次访问:第一次取得指针(引用变量),第二次获取堆内存的内容。而取得栈内存中的内容只需要一次访问。
四 方法区
方法区和堆内存是一样是各个线程共享的,它存储的是被加载的类信息,常量池,方法列表,静态变量等。方法区的大小并非固定的,可以根据需要进行动态调整。方法区的内存地址空间也不必须是连续的,JVM允许用户和程序制定方法区的初始大小,最大和最小尺寸。
其中,类信息包含了类和接口的名称。在方法区中,每一个类都维护着一个被称作常量池的一块内存区,存储的是String, Integer, floating point等类型的常量。其中String类的常量在常量池中是通过表的方式存储对,,有一张固定长度的CONSTANT_String_info表存储字符串值。这些数据和堆内存中的对象实例与数组一样,是通过索引来访问的。方法列表中保存着方法的名称已经它的描述符。
例子3:字符常量
public class HeapAndStackDemo01 {
public static void main(String[] args) {
// TODO Auto-generated method stub
String a = "haha";
String b = "haha";
System.out.println(a==b);
String c = new String("haha");
String d = new String("haha");
System.out.println(c==d);
System.out.println(a==d);
}
}
上面这个例子可以看出,c,d这两个引用变量指向了两块不同的堆内存,而a,b这两个指向了常量池中的同一个的地址。