Java 杂谈

深入了解java虚拟机(1)-内存方面的知识

2018-01-22  本文已影响0人  白虎先生

一、java虚拟机内存各个区域总结

java虚拟机在运行时会把它管理的内存分为几个不同的数据区域,基本上可以分成两个部分,一个是由所有线程共享的数据区域,另外一个数据区域就是线程自身的数据区域。其中,共享的数据区域包括方法区和堆,线程自身的数据区域有程序计数器、虚拟机栈和本地方法栈。

image.png

程序计数器

程序计数器可以理解为当前线程执行字节码的指示器,即可以记录下一条要执行的字节码,此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

虚拟机栈

java虚拟机栈是用于存储局部变量表、操作数栈、动态链接、方法出口等,这里的局部变量表存储了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用类型和returnAddress类型。这里的long和double类型的数据占用了2个局部变量空间(Slot),其余的数据类型只占用1个。在这个区域,会出现两种异常情况:一是StackOverFlowError,这是当线程请求的栈深度大于虚拟机所允许的深度所抛出的异常,二是OutOfMemoryError,这是当无法申请到足够的内存时会抛出的异常。

本地方法栈

本地方法栈和虚拟机栈是类似的,区别在于虚拟机栈是为虚拟机执行java方法服务的,本地方法栈是为native方法服务的,对于sun hotspot虚拟机,就直接把本地方法栈和虚拟机栈合并到一起。

java堆

在虚拟机规范中,所有的对象实例以及数组都是在堆上分配的,但是现在随着jit编译期的发展以及逃逸分析技术的成熟,这个规定也不是绝对的了。java堆是垃圾收集器主要的收集区域,也就是我们平常gc的主要收集区域。

方法区

方法区一般用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。如果是HotSpot虚拟机,会把方法区称为“永生代”,但是像HotSpot这种做法,就更容易导致内存溢出的问题。在JDK1.7中,就把原本放在永生代的字符串常量池移出了。

运行时常量池

运行时常量池是方法区的一部分,运行时常量池对于Class文件常量池的一个重要特征是具备动态性,即常量不一定在编译期就产生,在运行期间也可以将新的常量放入池中,例如String类的intern()方法。

二、从jvm角度去理解equals和==的区别

(1)对于java基本类型(boolean、byte、char、short、int、float、long、double),应该使用“==”来比较,比较的是他们的值。对于复合类型,使用“==”比较的是它们的在内存中的存放地址,使用equals方法是比较对象在堆内存的地址,但在一些诸如String、Integer、Date类中把Object中的这个方法覆盖了,作用被覆盖为比较内容是否相同。由于jdk1.7已经把常量池从方法区移除来了,这里只总结jdk1.7之后的区别。(也会加上jdk6的比较,但是笔者没有去验证,仅引用其他人的结论)
(2)在jdk6,字符串常量池是在方法区,也就是说,对于字符串常量,是在方法区分配的内存,而在jdk7之后的版本,是在java堆分配的内存,跟对象实例是在同一个地方分配的内存。还有要记住一点,通过双引号定义的字符串是直接在字符串常量池产生的,而通过new Stirng()方法产生的字符串是在java堆分配空间的。

(1)利用“==”比较双引号定义的String类型
    /**
     * "=="对于复合类型是比较地址
     */
    public void method1(){
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2);
    }
结果:true,因为s1和s2引用的是同一个字符串,地址是一样的
(2)利用“equals”比较双引号定义的String类型
    /**
     * "equals"本来是比较地址,但是Sting重写了,变成比较值了
     */
    public void method2(){
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1.equals( s2 ));
    }
结果:true,equals比较的是值,固两者是一样的。
(3)双引号定义的字符串和new String()字符串的比较
    public void method3(){
        String s1 = "hello";
        String s2 = new String("hello");
        System.out.println(s1 == s2);
        System.out.println(s1.equals( s2 ));
    }
结果:false和true,我们重点来看这种情况。
1、对于第一句话,是先在栈中创建了一个String类型的对象引用变量s1,然后检查常量池是否有“hello”这个字符串了,
如果有,就让s1指向“hello”,如果没有,就先在常量池创建“hello”字符串,然后让s1指向“hello”。
2、对于第二句话,也是先在栈中创建了一个String类型的对象引用变量s2,然后在堆内存中创建一个对象(new string),
同时,如果字符串常量池没有该字符串,也会在常量池生成一个“hello”字符串。
在这里说明一下,对于jdk7,因为常量池已经在堆内存了,所以常量池不一定会放字符串本身,也可能是一个引用,引用堆内存里面的字符串,
这点,在下面的intern()方法会着重说明。

上面第三点所分配的内存示意图

image.png
首先,先在栈创建了一个String类型的对象引用变量s1,然后检查常量池是否有“hello”这个字符串了,现在没有,然后就在常量池创建“hello”;接着,在栈创建了一个String类型的对象引用变量s2,在堆内存中创建一个对象,该对象存的就是“hello”字符串,所以s1和s2的地址是不一样的,但是值是一样的,使用“==”来判断地址的时候,就会输出false,而使用equlas来判断值的时候,就会输出true。

三、从jvm角度去理解String类的intern()方法。

首先,我们先要知道intern()方法的作用,intern()方法的目的在于复用字符串对象以节省内存,我们可以简单地理解为,对一个字符串使用intern()方法的时候,它会去字符串常量池查找该字符串是否已经存在,如果已经存在,就会直接返回常量池中的该字符串。

image.png
我们可以直接看Stirng.intern()方法的api文档的解释,上图红框部分的意思是,当调用intern()方法的时候,如果常量池里已经包含一个字符串,并且该字符串等于调用intern()方法的字符串对象(两者是通过equals方法来判断是否相等的),就返回常量池中的字符串,否则,就把该字符串对象加入到常量池中,并且返回该字符串对象的引用,因此,对于两个字符串s和t,s.intern() == t.intern()当且仅当s.equals(t)为真

jdk6的intern()方法详解

String s = new String("1");  
String s2 = "1";  
s.intern();  
System.out.println(s == s2);  
  
String s3 = new String("1") + new String("1");  
String s4 = "11";  
s3.intern();  
System.out.println(s3 == s4);  

在jdk6中,上面的结果都是false,解释如下:
先看前面四句话:
(1)s在堆内存创建了一个“1”对象,然后在字符串常量池也生成了一个“1”;
(2)s2发现字符串常量池已经有“1”了,然后就会直接指向“1”;
(3)s.intern()方法,会使得s的object对象指向常量池中的“1”
(4)所以,s和s2的地址是不一样的,所以第一个输出false。
再看后四句话:
(1)s3先在堆内存创建一个“1”对象,然后发现在字符串常量池已经有“1”了,就不会再次生成“1”,此时常量池是没有“11”字符串的。
(2)s4会在字符串常量池生成了一个“11”,然后让s4指向“11”
(3)s3.intern()方法,会使得s3的object对象指向常量池中的“11”
(4)所以,s3和s4的地址是也不一样的,所以第二个输出false。

image.png

如果我们把intern()方法跟前面的代码调换,在jdk6中,返回的结果是一样的,都是false,这个就不分析了,这里跟前面不同的一点在于,调用s3.intern()方法时,由于常量池没有“11”字符串,就会在常量池创建“11”字符串。

String s = new String("1");  
s.intern();  
String s2 = "1";  
System.out.println(s == s2);  
  
String s3 = new String("1") + new String("1");  
s3.intern();  
String s4 = "11";  
System.out.println(s3 == s4);  

结论:这里无论调用还是不调用intern()方法,结果都是false,因为地址都是不一样的,但是在jdk7就不一样了。

jdk7的intern()方法详解

String s = new String("1");  
String s2 = "1";  
s.intern();  
System.out.println(s == s2);  
  
String s3 = new String("1") + new String("1");  
String s4 = "11";  
s3.intern();  
System.out.println(s3 == s4);  

在jdk7中,上面的情况也是返回两个false。从图片可以看出来,跟jdk6的情况基本上是一样的,没什么区别,这里就不注重解释,关键是下面把intern()方法跟上面代码调换一行的情况,就大大不同了。


image.png

如果我们把intern()方法跟前面的代码调换,在jdk7中,返回的结果就不一样了。

String s = new String("1");  
s.intern();  
String s2 = "1";  
System.out.println(s == s2);  
  
String s3 = new String("1") + new String("1");  
s3.intern();  
String s4 = "11";  
System.out.println(s3 == s4); 

此时,第一个会输出false,但是,第二个会输出true,为什么呢,我们从下图来看看解释。
第一个输出跟前面一样,没什么区别,我们重点看看第二个输出true的情况:
(1)s3先在堆内存创建一个“1”对象,然后发现在字符串常量池已经有“1”了,就不会再次生成“1”
(2)调用s3.intern()方法,如果是jdk6,就会在字符串常量池创建一个“11”字符串了,但是,对于jdk7,由于常量池就在堆内存,此时可以直接使用堆内存里已经存在的“11”字符串,就是s3的object对象,也就是说,字符串常量池不会创建“11”字符串,它只会创建一个引用,该引用就是s3的object对象
(3)s4也是直接指向了该引用,不会在常量池创建“11”字符串。
(4)所以,s3和s4的地址是一样的,所以输出true。

image.png
结论:在jdk7中,字符串常量池已经不需要时刻保存一份字符串了,相反的,它可以保存一份引用,该引用指向堆内存的一个字符串对象即可。

对于java内存方面的知识就介绍到这里了,笔者对java内存的理解不深,上面所说如果有错误,欢迎指出。

参考资料
https://www.cnblogs.com/fengbs/p/7029013.html
http://blog.csdn.net/seu_calvin/article/details/52089040
http://blog.csdn.net/seu_calvin/article/details/52291082

上一篇下一篇

猜你喜欢

热点阅读