java内存分配详解

对于java程序员来说,在虚拟机的自动内存管理机制下,不需要显式的对new出来的对象进行free和delete操作。但是了解java是如何进行内存分配的,对我们排查错误具有极大的帮助。

今天笔者按照所看的书上的内容和自己的理解来简单的谈一下java的内存分配问题。关于java运行时的数据区域,主要涉及以下几个:

  • 程序计数器:它主要存储的是当期线程所执行的字节码的行号等信息,然后字节码解释器根据计数器中存储的内容来选取下一条需要执行的字节码指。因此,可以看出,每条线程都有一个独立的程序计数器,它是线程私有的。

  • java虚拟机栈和本地方法栈:这两个我放在一起来说,是因为他们两个的作用非常类似,区别不过是虚拟机栈是为虚拟执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务(就是这个方法是用C++/C来实现的,编译成dll供java调用的)。java虚拟机栈也是线程私有的,与线程的生命周期相同。程序中的每个方法在执行的时候都会创建一个栈帧,里面存放了局部变量表、操作栈、动态连接、方法出口信息,局部变量表里存的是程序中基本类型的数据,局部变量和对象的引用等等。每一个方法从调用到完成的过程,就是相应的栈帧在虚拟机栈中压栈和出栈的过程。

  • java堆:java堆是被所有线程共享的一块区域,它的作用就是存放几乎所有对象的实例,也就是存放new产生的数据(当然还有一些其他技术可以让new出来的数据存放到栈上或其他地方)。

  • 方法区:它也是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。

  • 运行时常量池:在方法区里面又划分了一块区域为运行时常量池,用于存放程序中的一切常量,包含代码中所定义的各种基本类型(如int,long等等)和对象型(如String及数组)的常量值(final)。每个Class文件都会有一个常量池,存放的是常量值的符号引用,当类加载后,会将这部分信息存放到运行时常量池,等到类的解析完成之后,会将符号引用替换成直接引用,与全局字符串池(String pool)中的值保持一致。

    下面我们来通过一些具体的例子来说明不同情况下java的内存分配情况:

    1
    2
    int a = 3;
    int b = 3;

编译器先处理int a =3;首先它会在当前方法的栈帧中的局部变量表中创建一个变量为a的引用,然后查找局部变量表中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b =3;在创建完b的引用变量后,因为在局部变量表中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。

这时,如果再令 a=4;那么编译器会重新搜索局部变量表中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

我们再看下面这个例子:

1
2
String str = new String("abc");
String str = "abc";

这两种方法都可以创建一个String对象,第一种方法会创建两个实例对象,一个是在类解析的时候,生成一个实例对象放到堆中,然后字符串池(String pool)中存放该实例的引用,第二个实例对象是在运行的时候用new来动态创建的。而第二种方法是直接在类解析的时候回生成一个实例对象放到堆中,然后字符串池(string pool)中存放该实例的引用。

当我们比较String对象的时候,用equals来比较两个String对象的内容是否相同,用==来比较二者的引用是否指向同一个对象,看代码:

1
2
3
String str1 = "abc";   
String str2 = "abc";
System.out.println(str1==str2); //true

可以看出str1和str2是指向同一个对象的。这是因为,在类解析的到String str1 = “abc”的时候就会在堆中创建一个”abc”对象,然后在全局字符串池(String pool)中存放这个引用,当解析到str2的时候会先在String pool中查询是否有”abc”这个值的引用中,如果有的话,就直接将这个引用值赋给str2,所以str1和str2指的是同一个对象。

1
2
3
String str1 =new String ("abc");   
String str2 =new String ("abc");
System.out.println(str1==str2); // false

用new的方式是生成不同的对象,每new一次生成一个。

总结一下就是说:使用诸如String str = “abc”并不能保证一定会创建对象,有可能只是指向一个已经创建好的对象,而通过new()方法是能保证每次会创建一个新对象。

再看下面的一个例子:

1
2
3
4
5
6
7
8
String s0="kvill";   
String s1=new String("kvill");
String s2="kv" + new String("ill");
String s3="kv" + "ill";
System.out.println( s0==s1 );//false
System.out.println( s0==s2 );//false
System.out.println( s1==s2 );//false
System.out.println( s0==s3 );//true

从上面可以看出s0是常量,在类解析的时候就被确定了。而”kv”和”ill”也都是字符串常量,当一个字 符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s3也同样在被解析为一个字符串常量,所以s3也是常量池中” kvill”的一个引用。而使用new String()则不是常量,不能在编译的时候被确定,所以他们有自己的地址空间,在堆中。

我们所说的运行时常量池,不只是包括在编译的时候产生的常量,也可以在运行的时候扩展,用的最多的方法就是String.intern()方法,当一个String实例str调用intern()方法时,Java 查找字符串常量池中 是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在字符串常量池中增加该对象的引用值,看下面示例:

1
2
3
4
5
6
7
8
9
String s0= "kvill";   
String s1=new String("kvill");
String s2=new String("kvill");
System.out.println( s0==s1 ); //false
s1.intern();
s2=s2.intern(); //把字符串常量池中"kvill"的引用值赋给s2
System.out.println( s0==s1); //false
System.out.println( s0==s1.intern() );//true
System.out.println( s0==s2 ); //true

上面例子比较容易懂,s2.intern()返回的是在常量池中”kvill”的引用,所以与s0是相等的。

再来看下面这个例子:

1
2
3
4
5
6
7
String a = "ab";   
String b = "b";
final String c = "b";
String ab = "a" + b;
String ac = "a" + c;
System.out.println((a == ab)); //result = false
System.out.println((a == ac)); //result = true

JVM对于字符串引用,在“+”号连接中,有字符串引用的存在,而引用的值在程序编译器是无法确定的,所以”a”+b的值只有在程序运行期来动态分配并将新的地址赋给ab,所以a!=ab,对于被final修饰的变量,在编译的时候被解析为常量值的变量会将该常量的引用值加入到自己的常量池中,所以此时”a”+c和”a”+”b”的效果是一样的,所以为true。

对于方法的调用,也是无法再编译器确定,如下:

1
2
3
4
5
6
7
String a = "ab";   
final String bb = getBB();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static String getBB() {
return "b";
}

对于字符串引用bb,它只有在程序运行期间调用方法后,将方法的返回值和”a”来动态连接并分配地址为b,所以上面的程序结果为false。

总结

  • java虚拟机栈是线程私有的,每个方法执行的时候都会创建一个栈帧,栈帧里面包含局部变量表、操作栈、动态连接、方法出口等信息,局部变量表里存的是程序中基本类型的数据,局部变量和对象的引用等等。每一个方法从调用到完成的过程,就是相应的栈帧在虚拟机栈中压栈和出栈的过程。
  • java堆是被所有线程共享的一块区域,它的作用就是存放几乎所有对象的实例,也就是存放new产生的数据。
  • 方法区也是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
  • 运行时常量池是属于方法区的一部分,用于存放程序中的一切常量,每个class文件都有一个常量池,当该类被加载的时候常量池里的信息就会储存到运行时常量池中,此时,class常量池与运行时常量池中都存放的是符号引用,当类被解析完成之后,符号引用替换为直接引用,与全局字符串池(String pool)中的引用值保持一致。

参考链接http://theopentutorials.com/tutorials/java/strings/string-literal-pool/