JAVA: 堆,栈,常量池

本片博客主要是粗略的对JVM中堆、栈、方法区的内容做个总结,更详细的内容,在本人马上学习了JVM的知识后,给大家分享,同时也方便自己以后复习学习;

特别感谢一下几篇博文,本文很多地方都是在这些博文的基础上加上自己的理解而写的,所以特此鸣谢如下:
》》Java内存分配之堆、栈和常量池 - Sara早安 - 博客园
》》Java常量池理解与总结
》》触摸java常量池
》》JAVA常量池理解与总结


1. 栈、堆、常量池的介绍

1.1 栈:

  在函数中定义的一些基本类型的变量及变量所对应的数据对象的引用变量(包括 在堆中由new创建的对象和在常量池中存在的基本类型的包装类和直接赋值的String类)都在栈内存中分配。
  当在一段代码块定义一个局部的基本类型变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

1.2 堆 :

  堆内存用来存放由 new 创建的对象数组;和 不满足常量池技术的基本类型的包装类的对象。在堆中产生了一个数组或对象后,还可以 在栈中定义一个特殊的变量(称其为引用变量),让栈中这个引用变量的取值等于数组或对象在堆内存中的首地址。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

  引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍 然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。 实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!

1.3 常量池 :

  常量池存在于JVM的方法区中,虚拟机必须为每个被装载的类型维护一个常量池。主要分为两种:一种是在加载class文件中的一些数据也就是编译期产生的常量,另一种是运行时产生的常量;
  Class文件中的常量池:指的是在编译期被确定,并被保存在已编译的.class文件中的一些字面量如文本字符串,声明为final的常量值等,还包含一些以文本形式出现的符号引用,比如:

  • 1.类和接口的全限定名;
  • 2.字段的名称和描述符;
  • 3.方法和名称和描述符。

      主要包含: 实现了常量池的、并由基本类型的包装类直接声明初始化的变量(例如 Integer i =120); 由String 直接初始化的变量 (例如String str =”abc”); 对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引 用。例如 String str =“abc”; 在常量池中开辟一个内存把字符串的值“abc”存在这个内存中,而他的符号引用 String str是存在栈中的,并且这个引用变量的值是“abc”在常量池中的地址。

堆与栈的比较:

  Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、 anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态 分配内存,存取速度较慢。

  栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。

数据共享:

  对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。


以字符串和基本类型及其包装类来详细分析这些问题:

2. 字符串的内存分配:

对于字符串,其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的,例如String str =”aaa” )的字符串的值也就是”aaa” 就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。注意String类重写了equals方法了比较的是两个对象的内容;

String s1 = "china";
String s2 = "china";
String s3 = "china";

String ss1 = new String("china");
String ss2 = new String("china");
String ss3 = new String("china");

System.out.println("s1==s2  :" + (s1==s2));// true
System.out.println("s1==s3  :" + (s1==s3));// true
System.out.println("s2==s3  :" + (s2==s3));// true
System.out.println("ss1==ss2  :" + (ss1==ss2));//false
System.out.println("ss1==ss3  :" + (ss1==ss3));//false
System.out.println("ss2==ss3  :" + (ss2==ss3));//false

这里写图片描述

细节分析:
对于String s1="china"; String s2="china"; String s3="china";创建过程解析:
首先当JVM都到String s1="china" 时,会在 栈 中定义一个 名字为 s1 的对String类型的 引用变量 String str;然后 会去常量池中查找是否有值为“china”的一块内存存在,如果没有找到,则开辟一块内存,并且字符串的值“abc”存入,然后将这块内存地址赋给栈中的引用变量,也就是引用变量s1的值常量池中“china”的内存地址;如果找到了,则直接将这块内存地址的值赋给栈中的引用变量;接下来,当JVM读到String s2="china" 时,通用会在栈中定义一个引用变量s2, 然后去常量池中查找,此时发现有“china”的内存块存在,则直接将这个内存块的地址返给栈中的引用变量 s2;接下来相应的有s2;所以他们三个存放的是同一块地址;

这里解释一下黄色这3个箭头,当JVM读取到String ss1 = new String("china") 也就是对于通过new产生一个字符串时,JVM会 在 栈 中定义一个名字为 ss1 的对String类型的 对象引用变量 String ss1 ; 然后会去常量池中查找是否已经有了“china”对象,如果没有则在常量池中创建一个此字符串对象,如果存在,然后在堆中再创建一个常量池中此”china”对象的拷贝对象。

  思考String str = new String("xyz") ; 产生几个对象?一个或两个,如果常量池中原来没有”xyz”,就是两个。

  思考: String 的 public native String intern() ; 方法,
  存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String 的 intern() 方法就是扩充常量池的 一个方法;当一个 String实例 str 调用 intern() 方法时,JVM 查找常量池中是否有相同Unicode的字符串常量;如果有,则返回其引用;如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用。
  

String s0= "kvill";   
String s1=new String("kvill");   
String s2=new String("kvill");   
System.out.println( s0==s1 );   // false  
s1.intern();   
//虽然存在但是没有接受返回的目标,此时s1还是指向堆中的对象
s2=s2.intern(); 
//存在,所以把常量池中"kvill"的引用赋给s2,此时s2存的不是堆对象的地址而是常量池中字符对象的地址; 
System.out.println( s0==s1); //false   
System.out.println( s0==s1.intern() ); //true   
System.out.println( s0==s2 ); //true;
2.2 字符串 拼接符合 “+”引起的几个问题

以下的内容有的是自己推理出来的,因为还没有仔细研究反编译的东西,而且网上找到几篇博文,对这方面说的也不是特别清楚,如果问题,请看到的朋友多多指点,
jdk7会自动处理将 + 号链接字符串的情况:

"hello: "+msg

反编译:

(new StringBuilder()).append("hello : ").append(msg).toString()

也就是说 JVM 在对于编译期没法确定的其值到底是什么的字符串,在运行前会通过new StringBuilder()的方式进行连接处理;那么自然这个字符串对象是存在堆内存中;

1String a = "ab";   
String x = "b";   
String b = "a" + x;   
System.out.println((a == b)); //false 

【1】中,JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即”a” + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。而在运行期就会借住new StringBuilder() 来处理,自然会将结果放在堆内存中,所以上面程序的结果也就为false。

2String a = "ab";   
final String x = "b";   
String b = "a" + bb;   
System.out.println((a == b)); //result = true 

【2】对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的”a” + bb和”a” + “b”效果是一样的。故上面程序的结果为true。

3】
String a = "ab";   
final String bb = getBB();   
String b = "a" + bb;   
System.out.println((a == b)); //result = false   
private static String getBB() {  
return "b";   
}

【3】JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和”a”来动态连接并分配地址为b,故上面程序的结果为false。

String s1 = "str";
String s2 = "ing";

String s3 = "str" + "ing";
String s4 = s1 + s2;
System.out.println(str3 == str4);//false

String s5 = "string";
System.out.println(s3 == s5);//true
public static final String A;    // 常量A
public static final String B;    // 常量B
static {   
    A = "ab";   
    B = "cd";   
}   
public static void main(String[] args) {   
    // 将两个常量用+连接对s进行初始化   
    String s = A + B;   
    String t = "abcd"; 
    System.out.println(s==t) //false
}  

3. 基本变量类型及其包装类的问题分析

对于基础类型的变量和常量,变量和引用存储在栈中,常量存储在常量池中。

int i1 = 9;
int i2 = 9;
int i3 = 9;

final int INT1 = 9;
final int INT2 = 9;
final int INT3 = 9;

这里写图片描述

主要栈和常量池中的数值是可以共享的,对int i1=9; int i2=9; int i3 =9; 的详细分析:
JVM读到 int i1=9 首先它会在栈中创建一个变量为i1的引用,然后查找栈中是否有9这个值,如果没找到,就开辟一块内存并且将9存放进来,然后将 i1 指向 9 也就是将这块内存的地址赋给 i1。接着处理 int i2 = 9; 在创建完i2的引用变量后,因为在栈中已经有9这个值,便将i2直接指向9。这样,就出现了i1与i2同时均指向9的情况。最后i3也指向这个9。

3.2 基本类型的包装类在内存中的分配

Java的8种基本类型包装器(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除 Float 和 Double 以外, 其它六种都实现了常量池, 是它们只在 [-128, 127] 才使用常量池。而如果大于127 或小于-128 则不会使用常量池所以会直接在堆内存中创建对象。

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1==i2);//true 栈存引用变量,常量池存值

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3==i4);//false 栈存引用变量,堆中存值;

int i5 = 234;
int i6 = 234;
System.out.println("i5==i6 "+(i5==i6));//true 都存在栈中;

int i71 = 343;
Integer i72=343;
Integer i8 = new Integer(343);
System.out.println(i71==i72);//true 不知道为什么,只要是值相等就是true
System.out.println("why "+(i71==i8));//true
System.out.println("why : "+(i72==i8));//false 

Integer ii1 = 40;
Integer ii2 = 40;
Integer ii3 = 0;
Integer ii4 = new Integer(40);
Integer ii5 = new Integer(40);
Integer ii6 = new Integer(0);

System.out.println("ii1=ii2   " + (ii1 == ii2));//true
System.out.println("ii1=ii2+ii3   " + (ii1 == ii2 + ii3));//true
System.out.println("ii1=ii4   " + (ii1 == ii4));//false
System.out.println("ii4=ii5   " + (ii4 == ii5));//false
System.out.println("ii4=ii5+ii6   " + (ii4 == ii5 + ii6));//true   
System.out.println("40=ii5+ii6   " + (40 == ii5 + ii6));//true

Integer ii7 = 200;
Integer ii8 = 200;
int ii =0;
System.out.println(ii7==i8+ii);//true
Integer ii9 = new Integer(400);
Integer ii10 = new Integer(400);
Integer ii11 = new Integer(0);
System.out.println(ii9 == ii10+ii);//true

因为+这个操作符不适用于Integer对象,所以都进行自动拆箱操作,进行数值相加,即 ii10+ii = 400。然后Integer对象无法与数值进行直接比较,所以ii9自动拆箱转为int值400,最终这条语句转为400 == 400进行数值比较。

4. 成员变量和局部变量在内存中的分配

  对于成员变量和局部变量:成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。 形式参数是局部变量,
  局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。 成员变量存储在堆中的对象里面,由垃圾回收器负责回收。 如以下代码:

class BirthDate {
    private int day;
    private int month;
    private int year;

    public BirthDate(int d, int m, int y) {
        day = d;
        month = m;
        year = y;
    }
    // 省略get,set方法………
}

public class Test {
    public static void main(String args[]) {
        int date = 9;
        Test test = new Test();
        test.change(date);
        BirthDate d1 = new BirthDate(7, 7, 1970);
    }

    public void change(int i) {
        i = 1234;
    }
}

这里写图片描述

对于以上这段代码,date为局部变量,i, d, m, y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:

  • main方法开始执行:int date = 9; date 为局部变量,基础类型,引用和值都存在栈中。
  • Test test = new Test(); test 为对象引用,存在栈中,对象 (new Test())存在堆中。
  • test.change(date); i 为局部变量,引用和值存在栈中。当方法change 执行完成后,i 就会从栈中消失。
  • BirthDate d1= new BirthDate(7,7,1970);d1 为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
  • main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(), new BirthDate()将等待垃圾回收。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的内存分为常量池。 1. (Stack):内存用于存储方法调用时的局部变量、方法参数、方法返回值以及方法调用时的执行环境。是线程私有的,每个线程都有自己的空间。是一种后进先出(LIFO)的数据结构,它的内存管理自动进行,不需要手动分配和释放。当一个方法被调用时,会在上创建一个帧(Frame),帧包含了方法的局部变量和部分运行时数据。当方法执行完毕后,对应的帧会被销毁。 2. (Heap):内存用于存储Java对象实例。是所有线程共享的一块内存区域。Java中通过关键字"new"来创建对象,对象会被分配在内存中。是一种动态分配和管理的内存区域,需要手动进行垃圾回收。Java虚拟机会负责对进行自动的内存分配和释放。 3. 常量池(Constant Pool):常量池用于存储字符串常量、类和接口的全限定名、字段和方法的名称和描述符等常量。常量池是每个类或接口的一部分,在编译期间就被确定,并且保存在.class文件中。运行时,常量池的内容被加载到内存中的运行时常量池中,在程序执行过程中可以动态地添加、删除或修改常量池中的内容。 总结:用于方法调用和执行环境,用于存储对象实例,常量池用于存储字符串和常量。它们在Java中扮演着不同的角色,并且具有不同的生命周期和管理方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值