Fork me on GitHub

java内存管理以及常量池

java内存管理以及常量池

java的内存管理就是对象的分配与释放问题。

在java中内存的分配由程序完成的,而内存的释放是由垃圾回收器(Garbage Collection,GC)完成的。

java虚拟机将其管辖的内存大致分为了三个逻辑部分:

1.方法区(Method Area)
2.Java栈(Stack)
3.Java堆(Heap)


目录:

[TOC]


1.方法区

方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会在运行时改变。常量池、源代码中的命名常量、String常量、代码段和数据段、静态域(用来存放static定义的静态成员)都放在方法区。

常量池(constant pool)
常量池指的是在编译期就被确定,并保存在已编译的.class文件中的一些数据。处理包含代码中所定义的各类基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法和名称和描述符
  • 类和接口的全限定名

2.Java栈

Java栈是一个逻辑感念,特点是后进先出
一个栈的空间可能是连续的,也可能不是。用来保存局部变量的值,包括:1.用来保存基本数据类型的值;2.存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中。栈的优势:存取速度要比堆快。缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

3.Java堆

堆分配意味着随意的顺序,在运行时进行存储空间分配和收回的内存管理模型。Java堆用来存放动态产生的数据,比方通过new关键字创建出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法,因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动地收走这些不再使用的数据,但缺点是在运行时动态分配内存,存取速度较慢。


4.Java的内存垃圾回收机制

Java的内存垃圾回收机制是从程序的主要运行对象开始检查引用链,当遍历一遍发现没有被引用的孤立对象就作为垃圾回收。GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。监视对象状态是为了更加准确、及时地释放对象,而释放对象的根本原则就是该对象不在被引用


5.内存泄露

在Java中,内存泄露就是存在一些被分配的对象,这些对象有以下两个特点:

  1. 这些对象是可达的,即引用指向
  2. 这些对象是无用的,即程序以后不会再使用这些对象

如果满足这两个条件,这些对象就可以被判定为Java中的内存泄露,这些对象不会被GC所回收,但是它却仍然占用内存


6.字符串的内存分配与常量池

对于字符串,其对象的引用都是存储在栈中的,如果是编译期已经创建好的就存储在常量池(如:直接用双引号定义的或者final修饰并且能在编译期就能确定的),如果在运行期间才能确定好就存储在堆中(如:new关键字创建出来的)。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

如以下代码:

1
2
3
4
5
6
7
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hello";

String s4 = new String("Hello");
String s5 = new String("Hello");
String s6 = new String("Hello");

分析:s1、s2、s3在编译期就能确定,在创建对象时,现在厂里吃重寻找到相同的值,若没有,则在常量池中创建一个对象。对于通过new产生的一个字符串,会先去常量池中查找是否已经有这个对象(此处”Hello”),如果没有则在常量池中创建一个此字符串对象,然后堆中创建一个常量池中的此对象(此处“Hello”)的拷贝对象。

图1

若要问:?String s = new String(“Hello”)这个语句产生了几个对象?
答案:一个或两个,若果常量池中原来没有“Hello”,那就是两个。

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

如下代码:

1
2
3
4
5
6
7
8
9
String s0= "Java";
String s1=new String("Java");
String s2=new String("Java");

s2=s2.intern(); //把常量池中"Hello"的引用赋给s2

System.out.println( s0==s1);
System.out.println( s0==s1.intern() );
System.out.println( s0==s2);

运行结果

false
true
true


String常量池问题的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1
String s0 = "HelloWorld";
String s1 = "World";
String s2 = "Hello" + s1;
System.out.println((s0 == s2)); //结果为false

2
String s0 = "HelloWorld";
final String s1 = "World";
String s2 = "Hello" + s1;
System.out.println((s0 == s2)); //结果为true

3
String s0 = "HelloWorld";
final String s1 = getWorld();
String s2 = "Hello" + s1;

System.out.println((s0== s2)); //结果为false

private static String getWorld() {
return "World";
}

分析:
【1】中,JVM对于字符串引用(由于在字符串的”+”连接中),有字符串引用存在,而引用的值在程序编译期是无法确定的,即“Hello”+s1无法被编译期优化,只有在程序运行期来动态分配并将连接后的新地址赋给s2。

【2】和【1】中唯一不同的是bb字符串加入了final修饰,对于final修饰的变量,它在编译时被解析为一个常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的“Hello”+s1和“Hello”+”World”效果是一样的。

【3】JVM对于字符串引用s1,虽然它为final修饰,但是它的值在编译期无法确定,只有在运行程序期调用方法后,将方法的返回值和“Hello”来动态连接并分配给地址s2。

对于字符串的操作,“abc”+”def”为三个字符串对象,第三个为“abcdef”。也就是说,在Java中对字符串的一切操作,都会产生一个新的字符串对象。String创建字符串时不可变的,任何对String的改变都会引发新的String对象的生产,而不是那些不用的未被引用的对象则为作为垃圾回收。


7.基本类型的变量和常量在内存中的分配

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

如以下代码:

1
2
3
4
5
6
7
int i1 = 9;
int i2 = 9;
int i3 = 9;

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

如图

图2

编译器先处理int i1 = 9; 首先它会在栈中创建一个变量为i1的引用,然后查找栈中是否有9这个值,如果没找到,就将9存放进来,然后i1指向9。接着处理int i2 = 9;创建完i2的引用变量后,因为栈中已经有9这个值,便将i2直接指向9。这样,就出现了i1与i2同时均指向9的情况。最后i3也指向这个9。

基本类型和基本类型的包装类。基本类型有:byte、short、char、int、long、boolean。基本类型的包装类分别对应的是:Byte、Short、Character、Integer、Long、Boolean(注意区分大小写)

二者区别:基本类型体现在程序中的普通变量,基本类型的包装类是类,体现在程序中的引用变量。因此二者在内存中的存储位置不同:基本类型存储在栈中,而基本类型包装类存储在堆中。上边提到的这些包装类都实现了常量池技术,另外两种浮点数类型的包装类则没有实现。

如下代码:

1
2
3
4
5
6
7
8
9
10
Integer i1=10;
Integer i2=10;
Integer i3=new Integer(10);
Integer i4=new Integer(10);
System.out.print(i1==i2); //输出为true
System.out.print(i2==i3); //输出为false
System.out.print(i3==i4); //输出为false
Double d1=1.0;
Double d2=1.0;
System.out.print(d1==d2); //输出为false

分析:

  1. i1和i2均是引用类型,在栈中存储指针,因为Integer是包装类,由于Integer包装类实现了常量池技术,因此i1和i2的10均是从常量池中获取的,均指向同一个地址,因此i1=i2
  2. i3和i4均是引用类型,在栈中存储指针,因为Integer是包装类。但是由于他们各自都是new出来的,因此不再从常量池寻找数据,而是从堆中各自new一个对象,然后各自保存指向对象的指针,所以i3和i4不相等,因此他们所存指针也不同,所指向的对象不同。
  3. d1和d2均是引用类型,在栈中存储指针,因为Double是包装类,但Double包装类没有实现常量池技术,因此Double d1 = 1.0;相当于Double d1 = new Double(1.0);是从堆中new一个对象。

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

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


9.==与equals()

  • ==是一个运算符
  • Equals则是String对象的方法

比较无法就是两种情况:

  1. 基本数据类型的比较
  2. 引用对象的比较

1.基本数据类型比较

==比较两个值是否相等,相等则为true,否则为false;而equals()是String类的额方法,不用来比较基本数据类型

2.引用对象比较

==比较两个对象是否指向内存中的同一个对象,即指向同一个地址;而equals()则比较String对象的内容是否相同

注意

  • String是一个特殊的引用类型,对于两个字符串的比较,不管是==还是Equals,两者比较的都是字符串是否相同,例如下面的两个例子的输出结果都是true:

    System.out.println(“Hello”==”Hello”);
    System.out.println(“Hello”.equals(“Hello”));

  • 当你创建两个String对象时,内存中的地址是不相同的,你可以赋相同的值。对弈字符串的内容相同,引用地址不一定相同(相同的内容的对象地址不一定相同),反过来,引用地址相同则内容一定相同。

-------------本文结束感谢您的阅读-------------
您的支持将鼓励我继续创作!