Java底层的内存分配策略

Java内存分配的主要区域:

1、栈:存放基本类型的数据和对象的引用,对象本身是存放在堆中

2、堆:存放用new产生的对象数据

3、常量池:存放常量

通常常量池有两种形态:

运行时常量池:则是jvm虚拟机在完成类装载操作后,将*.class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

(方法区存放了一些常量、静态变量、类信息等,可以理解成class文件在内存中的存放位置)。

静态常量池:即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。

4. 静态域:存放在对象中用static定义的静态成员

5. 寄存器:是速度最快的存储场所,因为寄存器位于处理器内部,这一点和其他的存储媒介都不一样。不过寄存器个数是有限的。在内存中的寄存器区域是由编译器根据需要来分配的。我们程序开发人员不能够通过代码来控制这个寄存器的分配。

栈(Stack):

在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个局部变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。 

堆(Heap):

堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

通常在堆中产生了一个数组或对象后,还会在栈中创建一个特殊的变量,并且让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量我们就通常称为引用变量。引用变量可理解为给数组或者对象起的一个名称,在以后程序执行中可以使用栈中的引用变量来访问堆中存储的数组或对象。

指针:栈中的变量是指向堆内存中的变量

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

常量池(constant pool):

Java中的常量池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复创建相等变量时节省了很多时间。Java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数(Double、Float)类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值[-128,127]时才可使用常量池,也即对象不负责创建和管理大于[-128,127]范围的这些类的对象。

特点:常量池是在编译期被确定,会被保存在已编译的.class文件中。 

除了包含代码中所定义的各种基本类型(如int、long等)和基本类型对应的包装类型(Integer、Long等)以及对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:

1、类和接口的全限定名

2、字段的名称和描述符

3、方法的名称和描述符

虚拟机会为每种被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。在程序执行的时候,常量池会储存在方法区(Method Area),而不是堆中。

对于String常量,它的值是在常量池中(字符串常量池)。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_STRING_INFO表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。

比如:String s1 = "abc" 以这种方式声明的,值就是存储在常量池中。

String s2 = new String("abc")这种方式声明的,会把 new 出的字符串对象存储在堆里。并在栈里会创建一个对象的引用,并指向堆里字符串对象存储的首地址。

堆(Heap)与栈(Stack):

栈的优势是,存取速度比堆要快,仅次于寄存器,存在栈里的数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄(对象引用)。当代码运行出该变量作用域后,Java会自动释放掉为该变量所分配的内存空间

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

总结:栈和常量池中的对象是可以共享,对于堆中的对象不可以共享。栈中的数据大小和生命周期(变量的作用域)是可以确定的,当没有引用指向数据时,这个数据回收并释放存储空间。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。

代码分析:

Integer(Byte,Short,Integer,Long,Character,Boolean类似)

【1】Integer e = 127; //先检查 Integer常量池中是否有127 没有则将127存在常量池中,并在栈创建一个对象引用指向对应的地址
Integer j = 127;  //Integer常量池中是否有127 有127会在栈创建一个对象引用并指向对应的地址
System.out.println(e==j);// true  e==j比较地址,两者也是相等的。

【2】Integer e2 = 128;
Integer j2 = 128;
System.out.println(e2==j2);// false 

为什么这里等于false:之前说过常量池是有大小限制的。而Integer常量池的大小就是[-128,127]超过这个数值范围,就会把数据存储在堆中,并在栈创建一个对象引用指向对应的值的对象的首地址。所以这里就相当于Integer e2 = new Integer(128); Integer j2 = new Integer(128); e2==j2是比较地址。当前是false。

如果是Integer e2 = -129;  Integer j2 = -129; e2==j2也是false不信可以自己试一下。

【3】int n2 = 128;
int m2 = 128;
System.out.println("n2==m2:"+String.valueOf(n2==m2));//true 这里就是true。千万别弄混,这中情况是存储在栈里面

String

【1】String a = “test”;

String b = “test”;

String b = b+"java";

【1】a,b同时指向常量池中的常量值"test",b=b+"java"之后,b原先指向一个常量,内容为"test”,通过对b进行+"java" 操作后,b之前所指向的那个值没有改变,但此时b不指向原来那个变量值了,而指向了另一个String变量,内容为”test java“。原来那个变量还存在于内存之中,只是b这个变量不再指向它了。

【2】String ab = "ab";
String b = "b";
String ab2 = "a"+b;
String ab3 = "a"+"b";
System.out.println(ab==ab2);// false
System.out.println(ab==ab3);// true

【2】String ab2 = "a"+b; 由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配到堆中,并将连接后的新地址赋给栈里的引用ab2。所以结果为ab==ab2比较地址时为false

String ab3 = "a"+"b";也存在加号为什么为true?

String ab3 = "a" + "b";编译器将这个"a" + "b"进行优化,作为常量表达式String b = "ab",然后存储在字符串常量池中,存储时会先检查常量池中是否存在"ab",之前已经有String ab = "ab"; 常量池中存在"ab",然后ab3指向了该地址,并不会去新开辟一个内存空间去存储"ab",所以ab==ab3为true

【3】String ab = "ab";
final String b= "b";   
String ab4 = "a" + b;   
System.out.println(ab == ab2); // true 

【3】和【2】中唯一不同的是加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中(嵌入到它的字节码流中)。所以此时的"a" + bb和"a" + "b"效果是一样的。所以结果为true。

【4】String ab = "ab";
final String finalMethodb = getBB();   
String ab5 = "a" + finalMethodb;   
System.out.println((ab == ab4)); // false

private static String getBB() {  
        return "b";   
}

【4】JVM对于字符串引用finalMethodb 就算是加了final修饰,它的值也是在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态连接并分配地址为b,所以这里连接后的值"ab"是保存在堆里的,ab4引用指向的是堆里的地址,ab引用指向的是常量池中保存的"ab"的地址。所以结果为false。

【5】String ab = "ab";
String ab6 = "ab";
String ab7 = new String("ab");
System.out.println(ab==ab5);// true
System.out.println(ab==ab6);// false

【5】ab和ab5引用指向的都是常量池中存储"ab"的内存地址。所以相等。ab6引用指向的是堆中对象的存储地址。所以ab==ab6为false

【6】String s0= "java";

String s1=new String("java");

String s2=s1.intern();//s1 检查常量池,发现没有就拷贝自己的字符串进去

System.out.println(s2 == s1);//false

System.out.println( s2==s0);//true

System.out.println( s1==s1.intern());// false

【6】String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回一个引用,没有则添加自己的字符串进入常量池,注意:只是字符串值部分。这时会存在2份拷贝,常量池的部分被String类私有并管理,自己的那份按对象生命周期还在继续使用。

基础类型的变量和常量在内存中的分配

int i1 = 9;
int i2 = 9;

final int INT1 = 10;
final int INT2 = 10; 

对于基础类型的变量和常量,值和引用存储在栈中,常量存储在常量池中。i1和i2引用和值9都在栈里存储 INT1和INT2是在引用是在栈中,值10是在常量池中

猜你喜欢

转载自blog.csdn.net/qq360694660/article/details/83144247
今日推荐