美文网首页
Java堆、栈、方法区、常量池

Java堆、栈、方法区、常量池

作者: code希必地 | 来源:发表于2020-12-24 23:59 被阅读0次

1、Java栈

Java在函数中定义的基本类型(int,long,short,byte,float,double,boolean,char)的变量(局部变量和函数的形参)的引用和数据,以及对象的引用都放在栈中存储。

1、栈的特点

  • 1、存取速度快。仅次于CPU中的寄存器。
  • 2、每个线程都会有一个栈空间,不同栈之间不能直接访问,所以线程之间不能共享栈中的数据。
  • 3、存在栈中的数据是可以共享的。

比如我们定义int a=3;int b=3;a=4;
编译器先处理int a=3;首先会在栈中创建一个变量a的引用,然后在栈中查找有没有字面值为3的地址,如果有则将a指向这个地址;如果没有则开辟一块内存放字面值3,然后将a引用指向这个地址。
接着处理int b=3;,在栈中创建b的引用变量,由于在栈中字面值为3的地址已经存在,所以直接将b指向这个地址。这样a和b同时指向了字面值为3的这个地址。
接着处理a=4;,会在栈中查找字面值为4的地址,如没有就开辟内存存放字面值4,让a指向这个地址,如有就将a直接指向这个地址。
此时b依然等于3,不会等于4。这点和对象的引用不同,需要注意。

  • 4、如果栈内存中没有足够的空间可以使用,JVM会抛出java.lang.StackOverFlowError异常。
  • 5、栈中定义的变量,在超出变量的作用域后,Java会自动释放为变量所分配的内存空间,该内存空间可以立即被另作他用。

2、堆

堆中主要用于存放new出来的对象和数组。下面举例看下对象的实例化过程。

Person a=new Person ("123");
Person b=new Person ("123");

编译器会先执行Person a=new Person ("123");,会在堆中开辟内存存放创建的对象Person ("123"),在栈中创建变量a,将a指向对象的内存首地址,a就是该对象的引用。
接着执行Person b=new Person ("123");,虽然前面已经创建了对象Person ("123"),但是只要使用关键字new,就会在堆中开辟一块新的内存存放创建的对象,在栈中创建变量b,将b指向新对象的首地址。所以a和b指向并不是同一个对象。
如果定义Person c = a;a.setName("234")那么String name=c.getName()就等于“234”。
执行Person c = a;则变量a和c都会指向同一个对象,所以使用a变量修改对象中的内容时,c指向的对象的内容也会改变。

2.1、特点

  • 1、存取速度比栈中的慢。
  • 2、一个JVM只有一个堆内存,所以线程间是可以共享堆内存中的数据的。
  • 3、如果堆中内存不足,则会抛出java.lang.OutOfMemoryError异常。
  • 4、定义在栈中的变量a指向堆中的对象Person的首地址,在超出变量的作用域后,Java会自动释放a所分配的内存空间,此时就没有引用指向对象了,但是对象并不会马上被回收,需要等某个时间通过垃圾回收来回收内存。

3、常量池

常量池分为两种:静态常量池和运行时常量池。
静态常量池
静态常量池指的是在编译期确定,保存在class文件中的一些数据。常量池主要用于存放两大类常量:字面量(Literal)和符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串、声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 1、类和接口的全限定名;
  • 2、字段的名称和描述符;
  • 3、方法的名称和描述符。
    运行时常量池
    在运行时常量池是方法区的一部分,在JDK1.7之后运行时常量池从方法区中移出,放在堆中。
    常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

3.1、字符串常量池

对于字符串,其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于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");
s1==s2==s3; //true
ss1!=ss2!=ss3!=s1!=s2!=ss3; //true
ss1.eqauls(ss2);//true
ss1.eqauls(ss3);//true
ss1.eqauls(s1);//true
ss1.eqauls(s2);//true
ss1.eqauls(s3);//true
image.png
可以看出s1、s2、s3在编译期就被创建,并存入到了常量池中。编译器在执行String s1 = "china";时会先在常量池中查找是否存在字符串常量“china”,如果不存在就在常量池中new一个china字符串,存在就不new,然后让栈中的变量指向这个china字符串。因此常量池中只有一个china字符串对象,然后在执行String s2 = "china";String s3 = "china";时,会在常量池中找到china字符串,并让s2、s3指向它。
对于String ss1 = new String("china");在编译时并不会创建,在运行时,通过new产生一个字符串(假设为“china”)时,会先去常量池中查找是否已经存在字符串“china”,如果不存在则在常量池中创建一个“china”字符串对象,然后在堆中再创建一个常量池中的“china”对象的拷贝对象;如果常量池中存在,就直接在堆中创建一个常量池中此“china”对象的拷贝对象。

3.2、包装类实现了常量池技术

对于8种基本数据类型大部分都有自己的包装类,其中Byte,Short,Integer,Long,Character,Boolean都实现了常量池技术,;而Byte,Short,Integer,Long类型在装箱时会缓存了范围[-128,127]的数据到数组中,Character会缓存[0,127]范围的数据到数组中进行缓存。

  • 1、对于Integer来说,范围是[-128,127]的数在自动装箱时全部被自动加入到了常量池里面,具体可查看Integer.valueof(int i)方法。
    Integer.valueOf()
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
    return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

IntegerCache类

private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

实例

public class test {
    public static void main(String[] args) {
        Integer i1=10;
        Integer i2=10;
        System.out.println(i1==i2);
                System.out.println(i1.equals(i2));
    }
}
输出:
true
true
  • 2、使用new关键字创建Integer时,即使数据在范围[-128,127],也不会去缓存中查找,直接在堆中创建一个新的Integer对象。并不会像new String("123")可能需要在堆中或常量池中各创建一个对象。
Integer i1=new Integer(10);
Integer i2=new Integer(10);
Integer i3=10;
System.out.println(i1==i2); //false
System.out.println(i1.equals(i2)); //true
System.out.println(i1==i3); //false
System.out.println(i1.equals(i3)); //true

-3、当整数不在[-128,127]范围内时,就会在堆中创建对象。
看下面例子在内存中的分配。

 public void test() {

    int a1 = 9; //自动拆箱 Integer.intValue()
    int b1 = 9; //自动拆箱 Integer.intValue()

    final int A2 = 9;
    final int B2 = 9;

    Integer a3 = new Integer(9);
    Integer b3 = new Integer(9);

    Integer a4 = 9; //自动装箱 调用Integer.valueOf(int)
    Integer b4 = 9; //自动装箱 调用Integer.valueOf(int)
    
}

如下图


image.png

4、方法区

方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即编译器编译后的代码等数据。静态变量、常量在方法区,所有方法,包括静态和非静态的,也在方法区。

5、成员变量和局部变量在内存中的分配

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

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

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

  1. main方法开始执行:int date = 9; date局部变量,基础类型,引用和值都存在栈中。
  2. Test test = new Test();test为对象引用,存在栈中,对象(new Test())存在堆中。
  3. test.change(date); i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。
  4. BirthDate d1= new BirthDate(7,7,1970); d1为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
  5. main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(), new BirthDate()将等待垃圾回收.

相关文章

网友评论

      本文标题:Java堆、栈、方法区、常量池

      本文链接:https://www.haomeiwen.com/subject/ajlvnktx.html