`
gao_xianglong
  • 浏览: 461715 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

透过OOP-Klass模型来看实例变量与类变量的存储

阅读更多

《透过OOP-Klass模型来看实例变量与类变量的存储》

 

前言

很久没有写过博客了,但今天在Iteye上看见有朋友提问静态变量是存储在方法区还是存储在哪里的一篇帖子,然后又搜索了其他的一些相关帖子,看后心里不免有些蛋疼,十有八九很多人从心里根本不清楚JVM是如何存储的,或者说,很多人根本就没有区分开什么是变量存储什么是值存储,这2个压根就不是同一个概念,导致很多人产生疑问,那么笔者今天这篇博客,要的效果就是深入到JVM底层来分析这个问题。当然由于本人能力有限,难免会有语义错误的地方,请大家指出。

 

正文

言归正传,再谈存储之前,首先做一个扫盲。从虚拟机内部表示形式来看,变量可以划分为实例变量和类变量(即静态变量)。实例变量与对象实例相关,也就是说,任何一个实例变量,在使用之前,首先要做的事情就是目标类型必须要实例化,然后才能够使用;而类变量与对象实例无关,但是却以类相关,也就是说,使用一个被static关键字标示的类变量是不用实例化对象的,直接可用。

 

当大家明白实例变量和类变量之前的区别后,我们再接从语法层面开始讨论引用类型的变量和原始类型的变量之间的区别。引用类型的变量所持有的值是什么?相信大家都应该知道,所谓引用指的就是一个指向目标对象实例的类似于“指针”的东西,而原始类型的变量所持有的值就是实际的原始值。

 

关于变量的概念已经介绍了一大堆后,接下来我们来再来看看变量值的存储。以粗粒度的方式来看待变量的存储尽管有些不妥,但为了让更多不理解JVM底层的人能够看懂,也是情有可原。Java的运行时数据区中,栈帧中的局部变量表用于存储方法体内的局部变量和方法参数,如果变量的类型是原始数据类型,则在局部变量表中存储实际的原始值,反之存储对象引用(reference),当然还会存储returnAddress类型。那么如果是成员变量,就存储在Java堆区中(内存布局中的实例数据中)。简单来说,与线程上下文相关的存储在Java栈中,反之存储在Java堆中,这个是常识。

图1 对象访问定位

之所以先说变量值的存储而没有先说变量的存储,笔者是可以安排的,因为这2个概念是非常多的开发人员容易弄混淆的。变量仅仅只是一个存储介质,所负责的任务很简单,就是牵引数据存储到正确的位置上,但它自己本身没有任何能力存储数据,这一点要明确。这里要引入一个概念,那就是JVM中对象的底层表示形式,OOP-Klass模型。对象内存布局中的内存头是由instanceOopDesc来表示(数组类型则用arrayOopDesc对象来进行表示),而对象头中的元数据指针所指向的当前对象的目标类型则是由Klass中的instanceKlass对象进行表示(数组则用arrayKlass对象进行表示)。OOPKlass其实是2个相互独立但是却又彼此相互关联的模块,这2个模块均包含在/openjdk/hotspot/src/share/vm/oops模块中,那么OOP-Klass模型与对象的内存布局之间又有什么关系呢?在JVM中对象头就是由OOP对象instanceOopDesc来表示(数组类型则用arrayOopDesc对象来进行表示),而对象头中的元数据指针所指向的当前对象的目标类型则是由Klass中的instanceKlass对象进行表示(数组则用arrayKlass对象进行表示),用于在JVM中表示一个Java类的对等体。当明确OOP-Klass模型的作用后,请大家思考一下JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?如图1所示,JVM可以通过对象引用准确定位到Java堆区中的instanceOopDesc对象,这样既可成功访问到对象的实例信息,当需要访问目标对象的具体类型时,JVM则会通过存储在instanceOopDesc中的元数据指针定位到存储在方法区中的instanceKlass对象上。

 

扯了一大堆,说白了,类变量既然不予对象实例相关,那么它则存储在方法区中的instanceKlass对象内,当然仅仅只是变量!

7
7
分享到:
评论
7 楼 gao_xianglong 2014-11-06  
RednaxelaFX 写道
嗯标题写错了,oop写成了OPP。

改过来了
6 楼 RednaxelaFX 2014-11-05  
嗯标题写错了,oop写成了OPP。
5 楼 duoduodeai 2014-11-05  
点踩的死逼们,给点建设性意见
4 楼 gao_xianglong 2014-11-04  
那家渔村 写道
gao_xianglong 写道
那家渔村 写道
    博主的文章很好,赞一下!
    但是感觉博主的结论好像有点说得不太对,“说白了,实例变量就是存储在对象内存布局中的实例数据内?这里只是说变量,不是说变量持有的值!”。
    我的理解是,java对象就是分配在java堆中逻辑上连续的内存片(类似于结构体,其中也存在边界对其的填充)。而这个连续的内存片中存放的就是java对象的实例域(实例变量)的值,另外说一句,每个java对象都有2个机器字长的头,用来定位对象所属的类型信息。其中java对象的内存布局可以看这个http://www.importnew.com/1305.html帖子。
    另外,说到java对象的实例变量与类变量,就顺便提一下<clinit>与<init>方法。<clinit>是类初始化方法,用来初始化类型静态域的,<init>是实例初始化方法,是初始化对象实例域的。


谢谢提出的宝贵意见,文章内容作了小的调整。
在HotSpot中,除了java.lang.Class对象实例时存储在方法区外,基本上所有的对象实例都会存储在Java堆区中(当然不排除栈上分配和其余的堆外存储技术),这里的堆是个大概的概念,细化的话,基于线程安全的考虑,如果一个类在分配内存之前已经成功完成类装载步骤之后,JVM就会优先选择在TLAB(Thread Local Allocation,本地线程分配缓冲区)中为对象实例分配内存空间,TLAB在Java堆区中是一块线程私有区域,它包含在Eden空间内,除了可以避免一系列的非线程安全问题外,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。反之在Eden中分配,如果是大对象则直接在老年代中分配。

实例变量,语法层面又可以划分为成员和局部,作用域划分后,还要划分类型。总之成员变量的值才是存储在对象内存布局中的实例数据内,而局部变量都是存储在局部变量表内(引用类型的局部变量持有引用)。

"实例变量,语法层面又可以划分为成员和局部,作用域划分后,还要划分类型。总之成员变量的值才是存储在对象内存布局中的实例数据内,而局部变量都是存储在局部变量表内(引用类型的局部变量持有引用."
博主,最好不要这样进行类比,java语言中的实例域(也称为成员或者实例变量)与局部变量完全不是一个概念,就是两个层面的东西,不要混为一谈。java语言中的局部变量就是指方法内部声明的局部变量和方法的参数,编译时统一存在为方法字节码的局部变量表中。如果方法为非静态方法,则局部变量表的第一位为this引用。
java对象的实例域和方法的局部变量无论从语义上还是存储上都没有任何关系。实例域存放在java对象所在堆空间的连续地址上,java对象的内存分布已经给了贴子的地址。而局部变量编译是存放在方法字节码的局部变量表中,而运行时分配在线程栈空间中。
java对象的实例域和java方法的局部变量完全没有关系!
唯一相同的就是值的分配。如果实例域(局部变量)的类型为基本类型,那么存储的值就是字面量值;如果实例域(局部变量)的类型为引用类型,那么存储的就是该引用所“指向java对象在堆中地址值”的副本。java方法调用中,局部变量的传递方式也是如此。



可能我理解的有些“偏差”,谢谢提出的宝贵意见!
3 楼 那家渔村 2014-11-04  
gao_xianglong 写道
那家渔村 写道
    博主的文章很好,赞一下!
    但是感觉博主的结论好像有点说得不太对,“说白了,实例变量就是存储在对象内存布局中的实例数据内?这里只是说变量,不是说变量持有的值!”。
    我的理解是,java对象就是分配在java堆中逻辑上连续的内存片(类似于结构体,其中也存在边界对其的填充)。而这个连续的内存片中存放的就是java对象的实例域(实例变量)的值,另外说一句,每个java对象都有2个机器字长的头,用来定位对象所属的类型信息。其中java对象的内存布局可以看这个http://www.importnew.com/1305.html帖子。
    另外,说到java对象的实例变量与类变量,就顺便提一下<clinit>与<init>方法。<clinit>是类初始化方法,用来初始化类型静态域的,<init>是实例初始化方法,是初始化对象实例域的。


谢谢提出的宝贵意见,文章内容作了小的调整。
在HotSpot中,除了java.lang.Class对象实例时存储在方法区外,基本上所有的对象实例都会存储在Java堆区中(当然不排除栈上分配和其余的堆外存储技术),这里的堆是个大概的概念,细化的话,基于线程安全的考虑,如果一个类在分配内存之前已经成功完成类装载步骤之后,JVM就会优先选择在TLAB(Thread Local Allocation,本地线程分配缓冲区)中为对象实例分配内存空间,TLAB在Java堆区中是一块线程私有区域,它包含在Eden空间内,除了可以避免一系列的非线程安全问题外,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。反之在Eden中分配,如果是大对象则直接在老年代中分配。

实例变量,语法层面又可以划分为成员和局部,作用域划分后,还要划分类型。总之成员变量的值才是存储在对象内存布局中的实例数据内,而局部变量都是存储在局部变量表内(引用类型的局部变量持有引用)。

"实例变量,语法层面又可以划分为成员和局部,作用域划分后,还要划分类型。总之成员变量的值才是存储在对象内存布局中的实例数据内,而局部变量都是存储在局部变量表内(引用类型的局部变量持有引用."
博主,最好不要这样进行类比,java语言中的实例域(也称为成员或者实例变量)与局部变量完全不是一个概念,就是两个层面的东西,不要混为一谈。java语言中的局部变量就是指方法内部声明的局部变量和方法的参数,编译时统一存在为方法字节码的局部变量表中。如果方法为非静态方法,则局部变量表的第一位为this引用。
java对象的实例域和方法的局部变量无论从语义上还是存储上都没有任何关系。实例域存放在java对象所在堆空间的连续地址上,java对象的内存分布已经给了贴子的地址。而局部变量编译是存放在方法字节码的局部变量表中,而运行时分配在线程栈空间中。
java对象的实例域和java方法的局部变量完全没有关系!
唯一相同的就是值的分配。如果实例域(局部变量)的类型为基本类型,那么存储的值就是字面量值;如果实例域(局部变量)的类型为引用类型,那么存储的就是该引用所“指向java对象在堆中地址值”的副本。java方法调用中,局部变量的传递方式也是如此。
2 楼 gao_xianglong 2014-11-04  
那家渔村 写道
    博主的文章很好,赞一下!
    但是感觉博主的结论好像有点说得不太对,“说白了,实例变量就是存储在对象内存布局中的实例数据内?这里只是说变量,不是说变量持有的值!”。
    我的理解是,java对象就是分配在java堆中逻辑上连续的内存片(类似于结构体,其中也存在边界对其的填充)。而这个连续的内存片中存放的就是java对象的实例域(实例变量)的值,另外说一句,每个java对象都有2个机器字长的头,用来定位对象所属的类型信息。其中java对象的内存布局可以看这个http://www.importnew.com/1305.html帖子。
    另外,说到java对象的实例变量与类变量,就顺便提一下<clinit>与<init>方法。<clinit>是类初始化方法,用来初始化类型静态域的,<init>是实例初始化方法,是初始化对象实例域的。


谢谢提出的宝贵意见,文章内容作了小的调整。
在HotSpot中,除了java.lang.Class对象实例时存储在方法区外,基本上所有的对象实例都会存储在Java堆区中(当然不排除栈上分配和其余的堆外存储技术),这里的堆是个大概的概念,细化的话,基于线程安全的考虑,如果一个类在分配内存之前已经成功完成类装载步骤之后,JVM就会优先选择在TLAB(Thread Local Allocation,本地线程分配缓冲区)中为对象实例分配内存空间,TLAB在Java堆区中是一块线程私有区域,它包含在Eden空间内,除了可以避免一系列的非线程安全问题外,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。反之在Eden中分配,如果是大对象则直接在老年代中分配。

实例变量,语法层面又可以划分为成员和局部,作用域划分后,还要划分类型。总之成员变量的值才是存储在对象内存布局中的实例数据内,而局部变量都是存储在局部变量表内(引用类型的局部变量持有引用)。
1 楼 那家渔村 2014-11-04  
    博主的文章很好,赞一下!
    但是感觉博主的结论好像有点说得不太对,“说白了,实例变量就是存储在对象内存布局中的实例数据内?这里只是说变量,不是说变量持有的值!”。
    我的理解是,java对象就是分配在java堆中逻辑上连续的内存片(类似于结构体,其中也存在边界对其的填充)。而这个连续的内存片中存放的就是java对象的实例域(实例变量)的值,另外说一句,每个java对象都有2个机器字长的头,用来定位对象所属的类型信息。其中java对象的内存布局可以看这个http://www.importnew.com/1305.html帖子。
    另外,说到java对象的实例变量与类变量,就顺便提一下<clinit>与<init>方法。<clinit>是类初始化方法,用来初始化类型静态域的,<init>是实例初始化方法,是初始化对象实例域的。

相关推荐

Global site tag (gtag.js) - Google Analytics