Java当中的内存分配.docx
- 文档编号:9370224
- 上传时间:2023-05-18
- 格式:DOCX
- 页数:14
- 大小:25.10KB
Java当中的内存分配.docx
《Java当中的内存分配.docx》由会员分享,可在线阅读,更多相关《Java当中的内存分配.docx(14页珍藏版)》请在冰点文库上搜索。
Java当中的内存分配
Java当中的内存分配以及值传递问题内存解析
(2010-11-1819:
40:
37)
转载▼
标签:
java
内存分配
参数传递
值传递问题
内存解析
分类:
JavaSE
首先必须说明作为Java程序员对于内存只要有大致的了解就可以了,如果你对Java当中的某一个知识点在不需要分析内存分配过程的情况下可以掌握,那就大可不必去研究内存。
如果你对知识点已经掌握,那么你应该把更多的精力放在对业务逻辑的分析与设计上,这样的话你才可能这一行业走的更远。
好了废话不多说了,下面我带着大家先来简单的看一下Java当中所涉及的内存分配,接着我会以讲解Java当中的值传递问题,分析在代码执行的过程当中内存的状态。
一、Java当中所涉及到的内存分类
Java当中你知道这5种内存就够用了,下面对这5种内存里面所存放的数据做一解释。
① 栈内存:
它里面存放的是引用(也就是地址,Java当中的这个地址并非内存的物理地址,但是它通过这个地址找到它所指向地址的内容)还有就是基本类型的值以及方法的形参也是存放在栈内存当中的。
② 对内存:
它里面存放的是对象、引用、基本类型的值(对于引用和基本类型的值什么时候放在栈里什么时候放在堆里,在后面讲解Java当中的值传递问题,分析在代码执行的过程当中内存的状态的时候会说)。
③ 寄存器:
它里面存放的是中间运算的数字(对于这个我们可以忽略不去考虑它)。
④ 代码段:
顾名思义它里面放的就是程序的代码。
⑤ 池内存:
池里面方的是常驻内存反复利用的数据。
好了这就是Java当中常见内存以及它里面所存放的数据,下面我们通过讲解Java当中的值传递问题,分析在代码执行的过程当中内存的状态。
二、Java当中的值传递问题以及代码执行过程当中内存的状态
什么是值传递?
值传递就是Java当中参数传递的一种方式(而且也是唯一的一种方式,也就是说Java当中只有值传递),所谓参数传递就是在某个方法被调用的时候把一个实参传递给形参的过程。
下面我们通过分析下面代码执行过称中内存的状态来说明Java当中的参数传递以及为什么Java当中只有值传递。
代码清单:
(为了节省空间格式不是很规范)
定义学生类:
定义测试类:
测试结果:
为什么会有这样的结果?
下面我们分析一下这段代码执行过称当中内存的分配,相信问题将迎刃而解。
1、 我们运行TestPassing这类,虚拟机加载TestPassing这个类,虚拟机将这些代码存放到代码段当中(这里我们就不画出代码段的图示了,后面虚拟机调用任何方法(包括构造方法)都要先到代码段中去找,但是这比较简单也不是重点接下来的解析当中如果涉及到方法调用就不再说明了),然后虚拟机从代码段当中找到main()方法,开始执行代码。
此时虚拟机为main()创建栈内存,内存分配如下
2、 接着执行intage=20;这行代码,由于它是基本类型的局部变量所以直接把它的值20存在栈内存名字叫age,内存分配如下
3、 接着执行TestPassingtp= new TestPassing();这一行代码,这句话在内存当中做了3个操作,首先TestPassingtp,tp是一个引用类型的变量所以给它分配一块栈内存存放一个TestPassing对象的引用(也就是地址假设这个地址是ox1a2b3c),接下来在堆内存创建一个TestPassing对象,接着把刚才栈里面tp的引用指向堆里面的这个TestPassing对象,这行代码的顺序之所以是这样是因为“=”的优先级比“new”的优先级低。
内存分配如下(在此只给出最终内存分配图)
4、 接着执行这一行代码tp.addAge(age);TestPassing对象调用addAge(intage)方法,虚拟机为addAge(intage)方法分配一个临时的栈内存,并且在这块临时栈内存当中为addAge(intage)的形参age也分配一小块栈内存,接着把main()当中的实参age的副本(注意是实参age的副本而不是实参age)传给形参age,由于实参age是基本类型所以实参age的副本就是20,也就是说把20传给形参age,此时的内存分配如下
这行代码到此还没有执行完,参数传过去之后接着程序跳到被调方法当中去执行,也就是执行age++;此时操作的是形参age与实参age没有任何关系,age++;完了之后形参age的值变成21,此时的内存分配如下
被调方法还没结束,程序接着往下执行到方法体的结束大括号,被调方法执行完毕,同时addAge(intage)的临时栈内存关闭。
此时的内存分配如下
5、 程序接着执行System.out.println("age="+age);(这一行代码的内存分配过程我想没人想让我画吧)这一行代码,很清楚看上面的内存图,也就不难理解为什么打印出20了。
6、 接下来程序执行到Students1= new Student();这一行代码和上面TestPassingtp=new TestPassing();内存分配的过称基本一样,这句话也是在内存当中做了3个操作,首先Students1,s1是一个引用类型的变量所以给它分配一块栈内存存放一个Student对象的引用(假设这个地址是ox1a2b3d),接下来在堆内存创建一个Student对象,它有一个int类型属性age,所以在刚才创建的对象的大块内存当中分出一小块来存放这个属性,里面存的值是0名在叫age(全局变量有默认值所以我们没给它赋值就默认为0),接着把刚才栈里面s1的引用指向堆里面的这个Student对象。
此时的内存分配如下
7、 接着程序执行s1.age=20;这一行代码,这行代码将堆内存当中的Student对象的age改为20,此时的内存分配如下
8、 接着程序执行tp.addAge(s1);这一行代码,TestPassing对象调用addAge(Students)方法,虚拟机为addAge(Students)方法分配一个临时的栈内存,并且在这块临时栈内存当中为addAge(Students)的形参s也分配一小块栈内存,接着把main()当中的实参s1的副本传给形参s,但是s1是引用类型它的副本就是它现在在栈内存里面的地址,也就是说把Student的地址传给形参s,所以形参就会根据这个地址找到Student对象,此时的内存分配如下
这行代码到此还没有执行完,参数传过去之后接着程序跳到被调方法当中去执行,也就是执行s.age++;此时它操作的时是真正的Student对象,所以这行代码执行完了之后Student对象的age属性就变成了21,此时的内存分配如下
被调方法还没结束,程序接着往下执行到方法体的结束大括号,被调方法执行完毕,同时addAge(Students)的临时栈内存关闭。
此时的内存分配如下
9、 程序接着执行System.out.println("s1.age="+s1.age);这一行代码,很清楚看上面的内存图,也就不难理解为什么打印出21了。
10、 main()结束,main()栈内存关闭,没有任何引用指向堆内存当中的TestPassing对象和Student对象,垃圾回收器回收资源,虚拟机关闭。
好了关于Java当中的内存分配以及值传递问题内存解析就说到这,Java当中的池内存也是一个很重要的概念,由于时间关系本次分析并未提及池内存,有时间再给大家分享。
可以给大家一个思考题,如果给addAge(int age) 这个方法再加一String类型的形参也就是把这个方法改成addAge(int age,Stringname)并在这个方法里面改变name的值,给Student类再加一个属性Stringname,并在addAge(Students)方法当中修改s.name的值,这样的话String是引用类型,那么name会怎样变呢?
java的各类型数据在内存中分配情况详解
分类:
收藏文章2013-09-1222:
46 192人阅读 评论(0) 收藏 举报
转载自:
有这样一种说法,如今争锋于IT战场的两大势力,MS一族偏重于底层实现,Java一族偏重于系统架构。
说法根据无从考证,但从两大势力各自的社区力量和图书市场已有佳作不难看出,此说法不虚,但掌握Java的底层实现对Java程序员来说是至关重要的,本文介绍了Java中的数据在内存中的存储。
2内存中的堆(stack)与栈(heap)
Java程序运行时有6个地方可以存储数据,它们分别是寄存器、栈、堆、静态存储、常量存储和非RAM存储,主要是堆与栈的存储。
【随机存储器:
RandomAccessMemory】
栈与堆都是Java用来在RAM中存放数据的地方。
与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。
另外,栈数据可以共享。
但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。
但缺点是,由于要在运行时动态分配内存,存取速度较慢。
【寄存器位于CPU中】
3Java中数据在内存中的存储
3.1基本数据类型的存储
Java的基本数据类型共有8种,即int,short,long,byte,float,double,boolean,char(注意,并没有string的基本类型)。
这种类型的定义是通过诸如inta=3;longb=255L;的形式来定义的,称为自动变量。
值得注意的是:
自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。
如inta=3;这里的a是一个指向int类型的引用,指向3这个字面值。
这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。
假设我们同时定义:
inta=3;intb=3;
编译器先处理inta=3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。
接着处理intb=3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。
这样,就出现了a与b同时均指向3的情况。
【上文提到了"引用+数值+内存地址"这三个名词,其中变量名就是引用,给变量赋的值就是数值,
而所提到的内存是抽象的内容,让引用指向的不是数值,而是存取数值的那块内存地址】
定义完a与b的值后,再令a=4;那么,b不会等于4,还是等于3。
在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。
因此a值的改变不会影响到b的值。
【定义变量,给变量赋值,然后在编译的过程中就可以将其保存在内存中了】
3.2对象的内存模型
在Java中,创建一个对象包括对象的声明和实例化两步,下面用一个例题来说明对象的内存模型。
假设有类Rectangle定义如下:
【Rectangle:
矩形】
classRectangle{
doublewidth,height;
Rectangle(doublew,doubleh){
width=w;height=h;}}
(1)声明对象时的内存模型
用Rectanglerect;声明一个对象rect时,将在栈内存为对象的引用变量rect分配内存空间,但Rectangle的值为空,称rect是一个空对象。
空对象不能使用,因为它还没有引用任何“实体”。
(2)对象实例化时的内存模型
当执行rect=newRectangle(3,5);时,会做两件事:
在堆内存中为类的成员变量width,height分配内存,并将其初始化为各数据类型的默认值;接着进行显式初始化(类定义时的初始化值);最后调用构造方法,为成员变量赋值。
返回堆内存中对象的引用(相当于首地址)给引用变量rect,以后就可以通过rect来引用堆内存中的对象了。
(3)创建多个不同的对象实例
一个类通过使用new运算符可以创建多个不同的对象实例,这些对象实例将在堆中被分配不同的内存空间,改变其中一个对象的状态不会影响其他对象的状态。
例如:
Rectangler1=newRectangle(3,5);
Rectangler2=newRectangle(4,6);
此时,将在堆内存中分别为两个对象的成员变量width、height分配内存空间,两个对象在堆内存中占据的空间是互不相同的。
如果有
Rectangler1=newRectangle(3,5);
Rectangler2=r1;
则在堆内存中只创建了一个对象实例,在栈内存中创建了两个对象引用,两个对象引用同时指向一个对象实例。
3.3包装类数据的存储
基本型别都有对应的包装类:
如int对应Integer类,double对应Double类等,基本类型的定义都是直接在栈中,如果用包装类来创建对象,就和普通对象一样了。
例如:
inti=0;i直接存储在栈中。
Integeri(i此时是对象)=newInteger(5);这样,i对象数据存储在堆中,i的引用存储在栈中,通过栈中的引用来操作对象。
【数据存储在堆中,引用存储在栈中】
3.4String类型数据的存储
String是一个特殊的包装类数据。
可以用Stringstr=newString("abc");的形式来创建;
也可以用Stringstr="abc";的形式来创建。
第一种创建方式,和普通对象的的创建过程一样;
第二种创建方式,Java内部将此语句转化为以下几个步骤:
(1)先定义一个名为str的对String类的对象引用变量:
Stringstr;
(2)在栈中查找有没有存放值为“abc”的地址,如果没有,则开辟一个存放字面值为“abc”的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。
如果已经有了值为“abc”的地址,则查找对象o,并返回o的地址。
(3)将str指向对象o的地址。
值得注意的是,一般String类中字符串值都是直接存值的。
但像Stringstr="abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用。
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
Stringstr1=“abc”;
Stringstr2=“abc”;
System.out.println(s1==s2);//true
注意,这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。
==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。
而我们在这里要看的是,str1与str2是否都指向了同一个对象。
我们再接着看以下的代码。
Stringstr1=newString(“abc”);
Stringstr2=“abc”;
System.out.println(str1==str2);//false
创建了两个引用。
创建了两个对象。
两个引用分别指向不同的两个对象。
以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
3.5数组的内存分配
当定义一个数组,intx[];或int[]x;时,在栈内存中创建一个数组引用,通过该引用(即数组名)来引用数组。
x=newint[3];将在堆内存中分配3个保存int型数据的空间,堆内存的首地址放到栈内存中,每个数组元素被初始化为0。
4内存空间的释放
栈上变量的生存时间受限于当前函数的生存时间,函数退出了,变量就不存在了。
在堆中分配的对象实例,当不再有任何一个引用变量指向它时,这个对象就可以被垃圾回收机制回收了。
5总结堆栈再来看Java的内存,栈内存用来存放一些基本类型的变量和数组及对象的引用变量,而堆内存主要是来放置对象实例的。
明白这个就能很好的解释多态、继承、覆盖方面的问题了
深入理解java虚拟机
(一)虚拟机内存划分
分类:
Java虚拟机2014-07-2222:
22 6318人阅读 评论(0) 收藏 举报
JAVA虚拟机内存heap栈
Java虚拟机在执行Java程序时,会把它管理的内存划分为若干个不同的数据区。
这些区域有不同的特性,起不同的作用。
它们有各自的创建时间,销毁时间。
有的区域随着进程的启动而创建,随着进程结束而销毁,有的则始终贯穿虚拟机整个生命周期。
Java虚拟机运行时内存区域主要分为七部分,分别是:
程序计数器,Java虚拟机栈,本地方法栈,方法区,Java堆,运行时常量池,直接内存。
如上图所示(图片来源于网络):
蓝色区域包裹的部分为运行时几个数据区域:
白色的部分为线程私有的,既随着线程的启动而创建。
每个线程都拥有各自的一份内存区域。
它们是:
JAVA栈(JAVA STACK),本地方法栈(NATIVE METHOD STACK),和程序计数器(PROGRAM COUNTER REGISTER)。
黄色部分是线程共享的,所有的线程共享该区域的内容。
他们是:
方法区(METHOD AREA),堆(HEAP)。
我们分别来介绍这些区域。
(1)程序计数器
程序计数器(program counter register)
学过计算机组成原理的都知道计算机处理器中的程序计数器。
当处理器执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。
与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。
此后经过分析指令,执行指令。
完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。
处理器的程序计数器是指寄存器,而java程序计数器是指一小块内存空间。
java代码编译字节码之后,虚拟机会一行一行的解释字节码,并翻印成本地代码。
这个程序计数器盛放的就是当前线程所执行字节码的行号的指示器。
在虚拟机概念模型中,字节码解释器工作室就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理等都依赖于它。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,因此为了线程切换后还能恢复执行位置,每条线程都需要一个独立的程序计数器。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Java Native方法,这个计数器值为空。
而且程序计数器是Java虚拟机中没有规定任何OutOfMemoryError的区域。
(2)虚拟机栈
Java虚拟机栈(VM Stack)也是线程私有的,因此它的生命周期也和线程相同。
它存放的是Java方法执行时的数据,既描述的是Java方法执行的内存模型:
每个方法开始执行的时候,都会创建一个栈帧(Stack Frame)用于储存局部变量表、栈操作数、动态链接、方法出口等信息。
每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
经常有人把Java内存分为堆内存和栈内存,这种是比较粗糙的分法,很大原因是大多数程序‘猿’最关注的,与对象内存分配最密切的区域就是堆和栈。
局部变量表存放的是编译器可知的各种基本数据类型(boolean 、byte、int、long、char、short、float、double)、对象引用(reference类型)和returnAddress类型(它指向了一条字节码指令的地址)。
其中64bit长度的long和double会占用两个局部变量空间(Slot),其余的数据类型只占用一个。
局部变量表所需的内存空间是在编译时期确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这部分区域规定了两种异常:
1、当一个线程的栈深度大于虚拟机所允许的深度的时候,将会抛出StackOverflowError异常; 2、如果当创建一个新的线程时无法申请到足够的内存,则会抛出OutOfMemeryError异常。
(3)本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是十分相似的,他们之间的区别不过是虚拟机栈为Java方法字节码服务,而本地方法栈则为Native方法服务。
在虚拟机规范中对本地方法使用的语言和使用方法与数据结构没有强制规定,因此具体的虚拟机可以自由实现它。
Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一。
和虚拟机栈一样,本地方法栈也会抛出OutOfMemoryError 和 StackOverflowError异常。
接下来我们介绍的都是所有线程共享的区域了。
(4)堆
堆(heap)是虚拟机中最大的一块内存区域了,被所有线程共享,在虚拟机启动时创建。
它的目的便是存放对象实例。
堆是垃圾收集器管理的主要区域,因此 很多时候也被成为‘GC’堆(Garbage Collected Heap)。
从垃圾回收的角度来讲,现在的收集器包括HotSpot都采用分代收集算法,所以堆又可以分为:
新生代(Young)和老年代(Tenured),再细致一点,新生代又可分为Eden、From Survivor空间和To Survivor空间。
从内存分配的角度来讲,又可以分为若干个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)。
当堆空间不足切无法扩展,会抛出OutOfMemoryError异常。
(5)方法区
方法去(Method Area)与Java堆一样,是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
它有个别名叫做非堆Non-Heap。
对于HotSpot开发者来说,很多人称它为“永久代”(Permanent Generation),但是两者并不等价,仅仅是因为HotSpot虚拟机设计团队把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以向管理堆一样管理这部分内存。
但是因为永久代有“-XX:
MaxPermSize的上限,使其更容易内存溢出。
因此在JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出去了。
当方法区无法满足内存分配需求的时候,会抛出OutOfMemoryError异常。
(6)常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Java 当中 内存 分配