前置知识
一个 完整的 Java 程序运行过程会涉及以下内存区域:
- 寄存器,JVM 内部虚拟寄存器,存取速度非常快,程序不可控
- 栈,保存局部变量的值,包括:1. 用来保存基本数据类型的值;2. 保存类的实例,的指针(即对堆区对象的引用);3. 加载方法时的帧(frame)
- 堆,存放动态产生的数据,比如 new 出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆里,但是它们是共享该类的方法,并不是每创建一个对象就把成员方法复制一次。
- 常量池,JVM 为每个已加载的类型维护一个常量池,常量池就是这个类型用到常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。池中的数据和数组一样通过索引访问。由于常量池中包含了一个类型所以的对其他类型、方法、字段的符号引用,所以常量池在 JAVA 的动态链接中起了核心作用。常量池存在于堆中。
- 代码段:用于存放从硬盘上读取的源程序代码
- 数据段:用来存放 static 定义的静态成员。
预备知识
- 一个 Java 文件,只要有 main 入口方法,我们就认为这是一个 Java 程序,可以单独编译运行
- 无论是普通类型的变量还是引用类型的变量(俗称实例),都可以作为局部变量,他们都可以出现在栈中。只不过普通实例的变量在栈中直接保存它所对应的值,而引用类型的变量保存的是一个指向堆区的指针,通过这个指针,就可以找到这个实例在堆区对于的对象。因此,普通变量只在栈区占用一块内存,而引用变量要在栈区和堆区各占用一块内存。
调用过程演示
public class BirthDate{
private int day;
BirthDate(int a,int b,int c){
}
public void setDay(int day){
this.day = day;
}
}
public class Test{
public void change1(int i){
i = 1234;
}
public void change2(BirthDate b){
b = new BirthDate(22, 2, 2004);
}
public void change3(BirthDate b){
b.setDay(22);
}
public static void main(String[] args){
// 1. JVM 自动寻找 main 方法,执行第一句代码,
//2. 创建一个 Test 类实例,在栈中分配一块内存,
//3. 存放一个指向堆区对象的指针zhizhen_test001
Test test = new Test();
//4. 创建一个 int 型的变量 date,由于是基本类型,直接在栈中存放 date 对应的值 9
int date = 9;
//5. 创建两个 BirthDate 的实例 d1,d2, 并在栈中存放了对应的指针指向各自的对象。它们在实例化时调用了有参数的构造参数,因此对象中有初始值。(注意,方法还是用同一份)
BirthDate d1 = new BirthDate(7, 7, 1970);
BirthDate d2 = new BirthDate(1, 1, 2000);
//6. 调用 test 对象 change1 的方法,并且以 date 为参数。JVM 读到这段代码时,检测到 i 是局部变量,因此会把 i 放到栈中,并且把 date 的值赋给 i。在 change1 的方法内,`i=1234`,所以 JVM 把 i 的值更新成 1234
test.change1(date);
//7. change1 执行完成后,立即释放变量 i 所占的内存空间。—— 这就是后进先出。
//8. 调用 test 对象的 change2 方法,以 d1 为参数。JVM 检测到 change2 方法中 b 参数为局部变量,立即把 b 加入栈中。由于 b 是引用类型的变量,它的值是 d1 中的指针,此时 b 和 d1 指向同一个堆中的对象。在 b 和 d1 之间传递的是指针。
//9. change2 的方法中又实例化了一个 BirthDate 的对象,并且赋值给b,`b= new BirthDate(22, 2, 2004)`。在内部执行过程是:在堆区 new 了一个对象,并且把这个对象指针保存在栈中b对应空间,此时实例不再指向实例 d1 指向的对象,但是实例 d1 所指向的对象并无变化,这样无法对 d1 造成任何影响。
test.change2(d1);
//10. change2 方法执行完毕后,立即释放局部引用变量 b 所占用的栈空间,注意只是释放了栈空间,堆空间要等待自带回收。
// 11.调用 test 实例的 change3 方法,以实例d2为例,JVM会在栈中为局部变量b 分配空间,并且把 d2 中的指针存放在 b 中,此时 d2 和 b 指向同一个对象。再调用实例 b 的 setDay 方法,其实就是调用 d2 指向的对象的 setDay 方法。
test.change3(d2);
// 调用实例 b 的 setDay 方法会影响 d2,因为二者存放的指针指向的是同一个堆区,因此 b 对属性的更改会影响 d2.因为两者指向的是同一个堆区。
// change3 方法执行完毕,立即释放局部引用变量 b
}
}
小结
- 变量分为两种:基本类型和引用类型。基本类型的变量和引用类型的变量作为局部变量,都放在栈里,基本类型直接在栈里保存值,引用类型只保存一个指向堆区的指针,真正的对象在堆里。作为参数时,基本类型就直接传值,引用类型传指针。
- 分清什么是实例什么是对象。
Class a = new Class()
,此时 a 叫做实例,对象就是 Class。实例 a 是局部变量,引用类型,栈中保存的是指向 Class 对象的指针。new Class() 对象保存在堆中。 - 栈中的数据和堆中的数据销毁不是同步的。方法一旦结束,栈中的局部变量会立即销毁,但是堆中对象不一定销毁。因为可能有其他变量指向了这个对象,直到栈中没有变量指向堆中的对象时,它才销毁,而且还不是马上销毁,要等垃圾回收扫描时才可能被销毁。
- 以上的栈、堆、代码段、数据段等等都是相对于应用程序而言的。每一个应用程序都对应唯一的一个 JVM 实例,每一个 JVM 实例都有自己的内存区域,互不影响。并且这些内存区域是所有线程共享的。这里提到的栈和堆都是整体的概念,这些堆栈还可以细分。
- 类的成员变量在不同对象中各有不同,都有自己的存储空间(成员变量在堆中的对象中)。而类的方法确实该类所有对象共享的,只有一套,对象使用方法时方法才压入栈,方法不使用则不占用内存
-
常量池。常量池它维护了一个已加载类的常量
常量池
+8大基本类型:byte, char, int, long, short, float , double, boolean,
- 基本类型的包装类:Byte,String,Integer,Short,Long,Boolean
基本类型和包装类的区别
基本类型存储在栈中,而基本类型包装类存储在堆中。上面提到的这些包装类都实现了常量池技术,另外两种浮点数类型的包装类则没有实现。另外,String 也实现了常量池技术。
public static void objPoolTest(){
int i = 40;
int i0 = 40;
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
Double d1 = 1.0;
Double d2 = 1.0;
String s = "s";
String ss = new String("s");
System.out.println("s == ss "+(s == ss));
System.out.println("i=i0 " + (i==i0));
System.out.println("i1==i2 "+ (i1==i2));
System.out.println("i1=i2+i3 "+(i1==i2+i3));
System.out.println("i4=i5 "+(i4==i5));
System.out.println("i4=i5+i6 "+(i4==i5+i6));
System.out.println("d1=d2 "+(d1==d2));
}
s == ssfalse
i=i0 true
i1==i2 true
i1=i2+i3 true
i4=i5 false
i4=i5+i6 true
d1=d2 false
- i 和 i0 都是普通类型 int 的变量,所以数据直接存储在栈中。
- 常量池维护的是,-128至127;这点和 python 一样
- String 也实现了常量池技术,添加新的 String 之前都先去检查常量池中是否已存在,存在则添加。
- new String("s") / new Integer(3) , 是生成了一个新的对象。new 的作用就是声明要在堆中开辟一块新的内存空间。