JVM内存模型及String对象内存分配

昨天看了一篇关于《Java后端程序员1年工作经验总结》的文章,其中有一段关于String和StringBuffer的描述,对于执行结果仍然把握不准,趁此机会也总结了下JVM内存模型。

1、JVM运行时数据区域

关于JVM内存模型之前也了解过一些,也是看过就忘,好记性比如烂笔头,记下来吧。参考此文章http://chenzhou123520.iteye.com/blog/1585224

图1 JVM运行时数据区域

(1)、程序计数器(Program Counter Register):

程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,是‘线程私有’的内存。

(2)、JAVA虚拟机栈(Java Virtual Machine Stack):

     与程序计数器一样,java虚拟机栈也是线程私有的,虚拟机栈描述的是Java方法执行的内存模型:在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型()、对象引用和returnAddress类型(指向了一条字节码指令的地址)

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

JVM Stack 异常情况:

StackOverflowError:当线程请求分配的栈容量超过JVM允许的最大容量时抛出

OutOfMemoryError:如果JVM Stack可以动态扩展,但是在尝试扩展时无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈时抛出

(3)、本地方法栈(Native Method Stack):

      本地方法栈与虚拟机栈所发挥的作用是非常相似,区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法(使用Java语言以外的其它语言编写的方法)服务。

本地方法栈也可以抛出StackOverflowError和OutOfMemoryError异常

(4)、JAVA堆(Java Heap):

虚拟机管理的内存中最大的一块,同时也是被所有线程所共享的,它在虚拟机启动时创建,此内存区域存在的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。这里面的对象被自动管理,也就是俗称的GC(Garbage Collector)所管理。用就是了,有GC扛着呢,不用操心销毁回收的事儿。

Java堆的容量可以是固定大小,也可以随着需求动态扩展(-Xms和-Xmx),并在不需要过多空间时自动收缩。

Java堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。

JVM实现应当提供给程序员调节Java 堆初始容量的手段,对于可动态扩展和收缩的堆来说,则应当提供调节其最大和最小容量的手段。

如果堆中没有内存完成实例分配并且堆也无法扩展,就会抛OutOfMemoryError。

(5)、方法区(Method Area):

跟堆一样是被各个线程共享的内存区域,用于存储以被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然这个区域被虚拟机规范把方法区描述为堆的一个逻辑部分,但是它的别名叫非堆,用来与堆做一下区别。

(6)、运行时常量池(Runtime Constant Pool):

     运行时常量池是方法区一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

2、String对象内存分配分析

先看以下代码,运行后,结果如代码1,2,3,4,5,6所示

[java] view plain copy

  1. package com.xtli.controller;
  2. public class StringTest {
  3. public static void main(String[] args) {
  4. String s1 = "hello";
  5. String s2 = "world";
  6. System.out.println(s1+"---"+s2);//1:hello---world
  7. change(s1,s2);
  8. System.out.println(s1+"---"+s2);//3:hello---world
  9. StringBuffer sb1 = new StringBuffer("hello");
  10. StringBuffer sb2 = new StringBuffer("world");
  11. System.out.println(sb1+"---"+sb2);//4:hello---world
  12. change(sb1,sb2);
  13. System.out.println(sb1+"---"+sb2);//6:hello---worldworld
  14. }
  15. public static void change(String s1, String s2) {
  16. s1 = s2;
  17. s2 = s1+s2;
  18. System.out.println("change(s1,s2)---"+s1+"---"+s2);//2:change(s1,s2)---world---worldworld
  19. }
  20. public static void change(StringBuffer sb1, StringBuffer sb2) {
  21. sb1 = sb2;
  22. sb2.append(sb1);
  23. System.out.println("change(sb1,sb2)---"+sb1+"---"+sb2);//5:change(sb1,sb2)---worldworld---worldworld
  24. }
  25. }

对以上代码进行分析说明,如下

[java] view plain copy

  1. public class StringTest {
  2. public static void main(String[] args) {
  3. //在main方法的栈中创建引用s1和引用s2,此引用s1和引用s2存放在栈(main方法的栈)中;编译时,在常量池中创建两个常量"hello"和"world",s1和s2分别
  4. //指向两个常量
  5. String s1 = "hello";
  6. String s2 = "world";
  7. System.out.println(s1+"---"+s2);//1:hello---world
  8. change(s1,s2);//引用s1和s2作为参数传递到change方法中
  9. //change方法中的引用s1,s2和main方法中的引用s1,s2存放地址并不同,以下输出的是main方法栈中的s1和s2,并没有发生变化,故代码3有以下输出
  10. System.out.println(s1+"---"+s2);//3:hello---world
  11. //以下两行代码将会在main方法栈中创建引用sb1和sb2,并在堆内存中创建两个对象"hello"和"world",sb1和sb2分别指向两个对象
  12. StringBuffer sb1 = new StringBuffer("hello");
  13. StringBuffer sb2 = new StringBuffer("world");
  14. System.out.println(sb1+"---"+sb2);//4:hello---world
  15. change(sb1,sb2);//引用sb1和sb2作为参数传递到change方法中
  16. //main方法中的sb1所指向的堆内存地址未发生变化,故仍为"hello",而change(sb1,sb2)方法改变了main方法中sb2所指向的堆内存地址的内容,故代码6有以下输出
  17. System.out.println(sb1+"---"+sb2);//6:hello---worldworld
  18. }
  19. public static void change(String s1, String s2) {//在change方法的栈中创建引用s1和s2,并指向常量池中的常量
  20. s1 = s2;//将引用s1指向s2的常量池中的"world"
  21. s2 = s1+s2;//在堆内存中创建"worldworld"对象,并将s2指向此堆内存地址
  22. System.out.println("change(s1,s2)---"+s1+"---"+s2);//2:change(s1,s2)---world---worldworld
  23. }
  24. public static void change(StringBuffer sb1, StringBuffer sb2) {//在change方法的栈(和上面的change方法栈不同)中创建引用sb1和sb2,并指向main方法栈中sb1和sb2所指向的对象
  25. sb1 = sb2;//将引用sb1指向sb2所引用的对象"world"
  26. sb2.append(sb1);//引用sb2所指向的对象发生变化,变为"worldworld",注意此时外部main方法中的sb2和此方法中的sb1均指向此堆内存地址,
  27. //此地址内容发生变化后,外部main方法中的sb2指向的内容也跟着变化
  28. System.out.println("change(sb1,sb2)---"+sb1+"---"+sb2);//5:change(sb1,sb2)---worldworld---worldworld
  29. }
  30. }

为了进一步说明change(String s1, String s2)中的结果,可以进行以下验证。

[java] view plain copy

  1. public static void change(String s1, String s2) {
  2. String s= "world";
  3. String ss= "worldworld";
  4. s1 = s2;
  5. System.out.println(s==s1);//输出true
  6. s2 = s1+s2;
  7. System.out.println(ss==s2);//输出false
  8. System.out.println("change(s1,s2)---"+s1+"---"+s2);//2:change(s1,s2)---world---worldworld
  9. }

故在change(String s1, String s2)方法中s1=s2后,s1所指向的是常量池中的"world",s2=s1+s2代码执行后,会在堆内存中重新创建对象,并将s2指向此堆内存地址。

以上均为个人总结,如有不正确之处,请指出,大家共同进步。

 

时间: 2024-10-23 11:45:33

JVM内存模型及String对象内存分配的相关文章

String对象内存分配和基本数据类型的默认值与初始化相关问题

题目: 阅读下面代码段,给出以下代码的输出结果. public class TestFunction { static int i; public static void main(String[] args) { String str1 = "abc"; String str = "abc"; String str2 = new String("abc"); System.out.println(str1 == str2); System.ou

Java之内存分析和String对象

http://www.cnblogs.com/devinzhang/archive/2012/01/25/2329463.html Java中内存分析: 栈(Stack) :存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中). 堆(heap):存放所有new出来的对象. 常量池(constant pool):在堆中分配出来的一块存储区域,存放储显式的String常量和基本类型常量(float.int等).另外

java线程内存模型,线程、工作内存、主内存

转自:http://rainyear.iteye.com/blog/1734311 java线程内存模型 线程.工作内存.主内存三者之间的交互关系图: key edeas 所有线程共享主内存 每个线程有自己的工作内存 refreshing local memory to/from main memory must  comply to JMM rules 产生线程安全的原因 线程的working memory是cpu的寄存器和高速缓存的抽象描述:现在的计算机,cpu在计算的时候,并不总是从内存读

jvm内存模型-回收算法-和内存分配以及jdk、jre、jvm是什么关系(阿里,美团,京东面试题)

1.什么是jvm?(1)jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的.(2)jvm包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域.(3)JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行.JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行. 2.jdk.jre.jvm是什么关系?(1)JRE(Java

Java内存模型-final域的内存语义

一 引言 说到final你肯定知道它是Java中的关键字,那么它所在Java中的作用你知道吗?不知道的话,请前往这篇了解下https://www.cnblogs.com/yuanfy008/p/8021673.html 今天我们来说说final域在JMM中的内存语义. 二 final域的重排序规则 开门见山,对于final域,编译器和处理器一定要遵守两个重排序规则(JSR-133才增强了final域): 1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这

(转)c#对象内存模型

对象内存模型 C#的对象内存模型写这篇博客的主要目的是为了加深自己的理解,如有不对的地方,请各位见谅. C#的对象内存模型: 一.栈内存和堆内存1.栈内存 由编译器自动分配和释放,主要用来保存一些局部变量.函数的参数等,例如,int a = 10 ,那么编译器会自动在栈上开辟一块内容用来存储变量a.2.堆内存 由程序员手动申请和释放,在C++中,通过new关键字申请,编译器不会释放,必须通过delete释放,对于C#,通过new 关键字申请,因为编译器的垃圾回收机制,程序员不需要手动释放内存.例

C++对象内存模型1(堆栈模型)

对象内存模型 一. 栈(Stack) VS. 堆(heap) 栈 由系统自动管理,以执行函数为单位 空间大小编译时确定(参数+局部变量) 函数执行时,系统自动分配一个stack 函数执行结束时,系统立即自动回收stack 堆  在c++中由程序员手动控制 手动分配new和malloc 手动释放delete和free 具有全局性,总体无大小限制 容易造成内存泄露 1. Myclass c(10); // 栈对象,空间大小在编译时确定,函数执行结束,系统立即回收 2. Myclass* func()

极客班直播课笔记1 C++对象内存模型(堆栈模型)

对象内存模型 一. 栈(Stack) VS. 堆(heap) 栈 由系统自动管理,以执行函数为单位 空间大小编译时确定(参数+局部变量) 函数执行时,系统自动分配一个stack 函数执行结束时,系统立即自动回收stack 堆  在c++中由程序员手动控制 手动分配new和malloc 手动释放delete和free 具有全局性,总体无大小限制 容易造成内存泄露 1. Myclass c(10); // 栈对象,空间大小在编译时确定,函数执行结束,系统立即回收 2. Myclass* func()

JVM内存模型详解

JVM内存模型也叫JVM运行时区域,是认识和了解JVM工作原理的基础,从java诞生以来,JVM内存模型基本保持着大同小异的整体形态,由此可见JVM内存模型是相当稳定的,直到jdk1.8之后JVM内存模型中才将permGen(永生代),也就是过去的方法区完全去除,使用metaspace取而代之,但是从整个JVM内存形态来说其实并没有产生太大的变化,有点"换汤不换药"的味道.除此之外JVM内存模型的设计原理也充分考虑了java程序的运行过程以及GC的策略,所以JVM内存模型是一个既基础又