java中的内存分配


java内存分配概述

java程序是在java虚拟机JVM上运行的,JVM相当于一个软操作系统,所有的java程序数据都被放进JVM包装的内存中,由JVM自身实现内存管理。

一般来说,一个java程序在运行时会涉及到以下几个内存区域:

  • 寄存器 :java内部虚拟寄存器,存取非常快。
  • 栈 :存放基本数据类型变量和对象的引用变量。
  • 堆 :存放动态产生的数据,比如new出来的对象。
  • 方法区 :各个线程共享的内存区域,用于存储class的二进制文件,包含了JVM加载的类信息、常量池、静态域以及编译后的代码等数据。(方法区在jdk1.8之后已被取代)
  • 常量池 :存放常量的地方。JVM为每个加载类型维护一个常量池,里面存放了该类型用到的常量集合。
  • 静态域 :存放在对象中用static定义的静态成员。
  • 程序计数器 :JVM执行程序的流水线,存放一些跳转指令。
  • 非RAM存储区 :硬盘等永久存储空间。

这里仅就java堆栈、常量池作一个简单的介绍,其它内存区域暂作了解。

java栈

在函数中定义的基本数据类型变量以及对象的引用变量都在函数的栈内存中分配,当该变量退出其作用域后,在栈中为该变量分配的内存空间将立即被释放并能立刻另作他用。

值得一提的java中的包装类,如Byte、Integer、Boolean等,这些由基本数据类型包装起来的类存放在堆中。

还有一点,这里的栈指的是java虚拟机维护的执行java代码所使用的栈,jvm的虚拟内存中还有一个本地方法栈,它与这里的栈是不同的,本地方法栈是jvm执行操作系统方法所使用的栈。

java堆

堆内存用来存储new出来的对象以及数组。一旦在堆中分配内存后,还可以在栈中定义一个引用变量指向这块内存区域,即类似这种语句:

 Person p = new Person();

以该语句为例,new Person()在堆中划分出一块内存区域来存储Person对象,Person p在栈中定义一个Person类型的引用变量,该引用变量保存堆中的Person对象的首地址,p相当于C语言中的指针,之后可以用p来访问堆中的Person对象。

当引用变量p超出它的作用域后,保存p的栈内存会被立即释放,但p指向的堆中的对象不会被立即释放掉。堆中的内存均由java虚拟机的自动垃圾回收器来管理,自动垃圾回收机制会不定时的扫描堆中内存,检测到垃圾就将之回收,而只有当堆中的内存没有引用变量来指向它们的时候才会成为垃圾,但也不是立刻被回收,java垃圾回收机制会在一个不确定的时间将这些垃圾清理掉。


java中栈和堆的区别

栈的存取速度快,速度仅次于寄存器,但在栈中存放的数据大小与生存期都已被事先确定,缺乏灵活性。

堆的存储的内容可以动态的分配内存大小,生存期也不必事先告诉编译器,java垃圾回收机制会自动回收不再被使用的数据,但缺点是由于要在运行时动态分配内存,存取速度较慢。

栈还有一个特性,即栈中的数据可以共享。
比方说我们定义如下语句:

int a = 7;
int b = 7;

编译器在执行int a = 7时,会先在栈内存空间中查找是否存在值为7的地址,如果没有,则新开辟一块int类型大小的空间并初始化值为7,之后令a指向这块地址。之后处理int b = 7时,因为栈空间中已经存在7这块地址,因此直接令b指向它。

a与b同时指向值为7的这块地址,但这并不意味着修改a的值,b的值也会随之变化,比方说我再令a = 9;这时编译器仍然会先查找栈中是否存在值为9的地址空间,如果没有,新开辟出来并令a指向它,但b的指向不会改变,b仍指向值为7的地址空间。

但,堆中的数据是不可共享的,如下:

Person p1 = new Person("tom",12);
Person p2 = new Person("tom",12);
System.out.println(p1 == p2);
//false

常量池

常量池指的是在编译期就被确定,并被保存在已编译的.class文件中的一些数据,除了保存基本数据类型和对象型的final常量值之外,还保存一些以文本形式出现的符号引用。

常量池分为两类,一类是静态常量池,一类是运行时常量池。

静态常量池就是class文件中的常量池,运行时常量池就是在class文件被加载进内存后,常量池被保存在了方法区中。
(注意,jdk1.8后永久代被移除,常量池转而在堆内存当中被维护。)

java虚拟机为每个被装载的类维护一个常量池,常量池就是该类型所用到常量的一个有序集合。


以字符串常量池为例。

举个例子:

String s = "abc";
String r = new String("abc");
System.out.println(s == r)
//false

String是一个包装类,在创建字符串时,形如上例的两种方式都是可行的,但同样都是字符串"abc",为什么比较的结果会是false呢?这是因为第一个"abc"存放在方法区常量池中,而第二个则存放在堆中。

因为第一个是在编译期就可以确定的,而第二个"abc"则需要在运行期动态创建。对于字符串来说,在编译器就可以确定的存放在常量池中,在运行期才确定的存放在堆中,它们的引用变量都存放在栈中。

常量池中的数据也是可以共享的,比如:

String s = "abc";
String r = "abc";
System.out.println(s == r)
//true

现在来分析一下用String创建字符串的两种方式的执行过程。

#1 String s = “abc”;

执行这条语句时,首先,在栈中定义一个String类型的引用变量s;
在常量池中查找是否存在值为"abc"的对象,如果没有,新创建出来并用s指向这块内存区域的首地址。

#2 String s = new String(“abc”);

对于用new产生的字符串对象,首先,在栈中定义一个String类型的引用变量s;
接着在常量池中查找是否存在值为"abc"的对象,如果没有,则在常量池中创建该对象,然后在堆中再创建一个常量池中此对象的拷贝对象。
最后,让s指向堆中该对象的内存首地址。

因此,对于在常量池中没有"abc"的情况下,String s = new String(“abc”);会创建两个对象。

intern()方法

String.intern()方法被用来动态扩充常量池,执行intern()方法时,java会先在常量池中查找是否有该对象,如果有,直接返回该对象的引用,如果没有,那么就在常量池中创建该对象,并返回它的引用。

举个例子:

String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s2==s3);
//true

运行结果为什么是true呢?首先,第一条语句创建了两个String对象,一个在常量池中,一个在堆中,而s1保存的是堆中的String对象的首地址。第二条语句会首先检查常量池中是否存在"abc"对象,显然存在,于是令s2指向该对象,第三条语句更不必多说,依据常量池的共享性,s3和s2均指向"abc"。


基本数据类型的内存分配

对于基本数据类型,如果是变量则存储在栈中,用final修饰的常量存储在常量池中。


成员变量和局部变量的内存分配

对于成员变量和局部变量来说,尽管它们都定义在类中,但在存储方式上也有很大区别。

其中,成员变量存放在堆中new出来的对象里面,由垃圾回收机制负责回收。局部变量存放在栈内存中,方法体结束后,方法体中的局部变量也将立即被释放。


ps:有关这方面的博客看了不下十几篇,但越看越头大,主要是讲的都有点深,实在是啃不下去,索性总结了一小点入门,以后慢慢来吧,贪多嚼不烂。

参考:
https://www.cnblogs.com/SaraMoring/p/5687466.html
https://www.cnblogs.com/doubleqsweet/p/6970200.html
https://www.cnblogs.com/syp172654682/p/8082625.html
https://blog.csdn.net/qq_26947195/article/details/79505553

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值