JVM内存管理 | String intern()分析

JVM内存管理    参考资料:《深入理解Java虚拟机》——周志明  著;

 

Java内存区域

JavaC/C++的内存管理是不同的。

Java将内存管理全权交给了JVM处理,而C/C++则是由程序员自己控制,虽然这样比较麻烦,但其内存管理相比于Java来说是更透明的,程序员更清楚的知道自己对内存做了啥。

而大多数入门Java的书籍对内存管理只以一笔带过,叫新手不用花心思去了解它,当然这种内存管理机制确实比较方便,但却更难控制。

1. JVM数据区 JDK1.6

数据区中的红色区域为线程共享的区域

数据区中的黑色区域为线程私有的区域

需要注意:在目前的JDK1.8中,保留了线程私有程序计数器、虚拟机栈、本地方法栈,但方法区,也就说常说的永久代被移除,常量池被放进堆中,原本存储在方法区中的对象类型数据由一个叫做元空间(metaspace)的内存区域保存,元空间是独立于各个内存区域依赖于本地内存的一个区域

 

 

1.1线程私有区域:虚拟机栈、本地方法栈、程序计数器

先要说明一下什么是本地方法(Native Method

Java中其实允许执行非Java编程语言编写的程序,这种非java方法叫作Native Method,调用Native Method将由JVM负责控制。

Java方法就是由Java编程语言编写的方法,而Native方法,也叫本地方法,一般是指是由JVM调用的本地中由C语言编写的方法,具体的Java引用C的方法这里不再讨论。

1.1.1程序计数器:

一块较小的内存区域,用于指示线程中执行的字节码的行号,并完成执行位置的各种跳转,因为线程切换后需要重新指示线程的执行位置,每条线程之间不能相互影响,故该内存区域为线程私有。

1.1.2虚拟机栈:

描述了Java方法执行的内存模型,也有叫它Java栈的说法(?)

方法执行时将创建一个栈帧用于存储方法内的局部变量表、动态链接、方法出口等信息。

调用一个方法代表着一个栈帧再虚拟机栈中入栈到出栈的过程。

一般所说的栈,就是指的虚拟机栈。

抛出异常情况:

StackOverflowError:方法请求的栈深度超过了虚拟机栈提供的最大深度。

OutOfMemoryError:虚拟机栈在动态扩展以满足方法请求的栈深度时,请求不到足够的内存,多发生于系统内存不足。

1.1.3本地方法栈:

控制了本地方法的执行,并不一定是C语言编写的方法,也可能是其他语言,具体的实现依照使用的虚拟机而定。

HotSpot虚拟机中本地方法栈和虚拟机栈合二为一。

抛出异常情况:

与虚拟机栈相同。

 

1.2线程公共区域:方法区、堆

 

1.2.1

所有的对象实例和数组在此分配内存,但也不是绝对的。

堆是垃圾收集管理的主要区域,为了更好的实现内存分配和垃圾收集,堆可以被细分为更小的部分,具体算法由所使用的JVM实现。

异常情况:

OutOfMemoryError:无法再为实例请求到内存时抛出

1.2.2 方法区

用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译的代码等数据。垃圾收集在此区域出现比较少,但并不意味着数据进入了此区域就不会被回收了。

运行常量池:存放编译期生成的各种字面量和符号引用。运行期间也可以把新的常量放入池中,这里有一个很有意思的方法, String 类的 intern() ,通过这个方法可以帮助理解 java 的内存模型,根据 JDK 的版本具体的实现会有些不同,详情查阅网络上的各种资料。
在JDK1.8中,常量池已被移除,使用元空间(metaspace)代替,元空间不属于方法区也不在堆内,是独立的一块依赖于本地内存的区域

原理图:

intern()

package ryo;
 
publicclass Test {
 
    publicstaticvoid main(String[] args) {
       //通过这种方式创建的字符串将被直接放入运行时常量池
       Strings1 = "abc";
       //当运行时常量池中已经存在了一个"abc"之后,s2的引用将直接指向已存在的"abc"
       Strings2 = "abc";
       //通过new关键字创建的String对象将被存储在堆中
       Strings3 = new String("abc");
       Strings4 = "a";
       Strings5 = "bc";
       //这里的两个字符串相加会被编译为:
       //String s6 = newStringBuilder(s4).append(s5).toString();实际上这里的s6已经是一个new出来的对象了
       Strings6 = s4+s5;       
       //同为常量池中的"abc"引用故相等
       System.out.println("s1 == s2 "+(s1 == s2));
       //s3不在常量池中 s1则是常量池的引用故不等
       System.out.println("s1 == s3 "+(s1 == s3));
       //s3不在常量池中 s2则是常量池的引用故不等
       System.out.println("s2 == s3 "+(s2 == s3));
       //s6是一个对象不在常量池中故不等
       System.out.println("s2 == s6 "+(s2 == s6));
       //"a"也是一个运行时常量和s4一样指向运行时常量池中"a"的引用
       System.out.println("a == s4 "+("a" == s4));
       //s3 和 s6都不在常量池,但堆中每个实例是独立的存储地址,故不等
       System.out.println("s3 == s6 "+(s3 == s6));
       //s6.intern()将把"abc"的引用放入常量池,返回引用,若常量池中已有"abc"则返回该"abc"的引用
       //这里把常量池中的"abc"引用赋值给s6,故s6和s2都是常量池中的"abc"引用,相等
       s6 = s6.intern();
       System.out.println("s2 == s6 "+(s2 == s6));
    }
}

运行结果:

s1 == s2 true

s1 == s3 false

s2 == s3 false

s2 == s6 false

a == s4 true

s3 == s6 false

s2 == s6 true

 

   

   

异常情况:

OutOfMemoryError:方法区无法满足内存分配的需求时抛出。

 

1.2.3直接内存

直接内存并不是JVM运行时数据区的一部分,常使用这块内存的地方的一个例子就是NIO

NIO的一个示例

NIO直接对独立于堆之外的直接内存进行读写,在一定量的高并发下相比于BIO有显著的性能提升。

直接内存的使用不受java堆的限制,但仍然受到本机的物理内存和处理器最大寻址的限制。

占用过高内存可能会导致数据区无法申请到需要的内存,从而引发OutOfMemoryError异常。



猜你喜欢

转载自blog.csdn.net/my_dearest_/article/details/79837601