-
内存模型图
java栈的栈帧:
堆:
-
c,c++与java的跨平台有什么区别?
1、 C# .net Java的跨平台是指它们在“编译系统”生成的中间文件的字节码(byte code)文件不需要重新编译,就可以直接被“运行系统”使用; C/C++的跨平台是指不要重新写代码,需要重新编译成“运行系统”对应的机器码(binary code)后,才能够被“运行系统”使用;
如下图所示(所有的名词的叫法以下图为准):
2、 Java, C#的跨平台是建立在“虚拟机”的基础上的,通过虚拟机在程序运行时将“编译系统”生成的字节码(byte code) 转换成“运行系统”的机器码(binary code),属于解释性语言; C / C++是在“编译系统”上直接将代码生成“运行系统”上的机器码(binary code),属于编译性语言;3、 java, C#的跨平台受“虚拟机”的限制,如:
-
1、.net环境只在windows上有,linux(Android)和unix(ios)都不支持;
-
2、Java手机平台主要是Android支持,windows(WP8)和mac(ios系统)都不支持;
-
3、.net ,和Java虚拟机在xp系统上默认是没安装的;
-
4、xp系统不支持.net的高版本,如.net 4.5
c/c++不受虚拟机的限制,只要能够编译成“运行系统”的“机器码”即可,这也是为什么现在大量的跨平台应用开发采用c/c++的原因;
注:跨平台的概念不仅独用于java,对跨平台的理解要更深入
-
栈和栈帧
栈:
1、又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。其特性是先进后出。
2、栈是线程私有的,生命周期跟线程相同,当创建一个线程时,同时会创建一个栈,栈的大小和深度都是固定的。
3、方法参数列表中的变量,方法体中的基本数据类型的变量和引用数据类型的引用都存放在栈中,成员变量和对象本身不存放在栈中。运行时,成员函数的局部变量引用也存放在栈中。
4、栈的变量随着变量作用域的结束而释放,不需要jvm垃圾回收机制回收。
5、栈不是全局共享的,每个线程创建一个栈,该线程只能访问其对应的栈数据
6、栈内存的大小是在编译期就确定了的。
栈帧:
1、一个栈中可以有多个栈帧,栈帧随着方法的调用而创建,随着方法的结束而消亡。该栈帧中存储该方法中的变量,原则上各个栈帧之间的数据是不能共享的,但是在方法间调用时,jvm会将一方法的返回值赋值给调用它的栈帧中。每一个方法调用,就是一个压栈的过程,每个方法的结束就是一个弹栈的过程。压栈都将会将该栈帧置于栈顶,每个栈不会同时操作多个栈帧,只会操作栈顶,当栈顶操作结束时,会将该栈帧弹出,同时会释放该栈帧内存,其下一个栈帧将变为栈顶。栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
2、栈中的优化,其一是当局部变量赋值时,会在栈空间中找其对应的值,当有该值时,将该值指向变量,当没有该值时,创建一个该值,然后再指向该变量,例如:int a = 1, int b = 1, b = 2; 其二是栈中的变量随着方法的调用而创建,当方法执行结束后,jvm会自动释放内存。
栈帧的组成部分:
1、局部变量表:是一组变量值的存储空间,用呀存放方法参数和局部变量,虚拟机通过索引定位的方式使用局部变量表。(详情见目录->局部变量表)
2、操作树栈:常称为操作数栈,是一个后入先出栈。方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的。在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。
3、动态连接: 在说明什么是动态连接之前先看看方法的大概调用过程,首先在虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用,如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法,这时候就有一点需要注意,如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
4、方法返回地址:方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法.不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定.在方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括,恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。
栈的优点:
1、栈帧内存数据共享:栈帧之间数据不能共享,但是同一个栈帧内的数据是可以共享的,这样设计是为了减小内存消耗,例如:int a= 1, int b= 1时,前面定义了a=1,a和1都在栈内存内,如果再定义一个b=1,此时将b放入栈内存,然后查找栈内存中是否有1,如果有则b指向1。如果再给b赋值2,则在栈内存中查找是否有2,如果没有就在栈内存中放一个2,然后b指向2。也就是如果常量在栈内存中,就将变量指向该常量,如果没有就在该栈内存增加一个该常量,并将变量指向该常量。
2、存取速度比堆要快,仅次于寄存器。速度快之一是栈在编译器就申请好了内存空间,所以在运行时不需要申请内存大小,节约了时间,其二是栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。其三是访问时间,访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正得数据,而栈只需访问一次。
栈的缺点:
1、存在栈的数据大小和生存期必须是确定的,缺乏灵活性。当栈在运行执行程序时,发现栈内存不够,不会动态的去申请内存,以至于导致程序报错,所以灵活性较差。
栈上分配:
JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。
栈上分配的一个技术基础是进行逃逸分析,逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。另一个是标量替换,允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。
只能在server模式下才能启用逃逸分析,参数-XX:DoEscapeAnalysis启用逃逸分析,参数-XX:+EliminateAllocations开启标量替换(默认打开)。在JDK 6u23版本之后,HotSpot中默认就开启了逃逸分析,可以通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
因为栈的空间比较小,所以栈上分配的对象只能是小对象(1M以下),大对象和逃逸对象是不能进行栈上分配的。
原文:
-
局部变量表
局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
在 Java 程序编译为 Class 文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位,虚拟机规范中并没有明确指明一个 Slot 应占用的内存空间大小,只是很有导向性地说到每个 Slot 都应该能存放一个 boolean、byte、char、short、int、float、reference(注:Java虚拟机规范中没有明确规定reference类型的长度,它的长度与实际使用 32 还是 64 位虚拟机有关,如果是 64 位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取 32 位虚拟机的 reference 长度)或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出 “每个 Slot 占用32位长度的内存空间”是有一些差别的,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发送变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让 Slot 在外观上看起来与 32 位虚拟机中的一致。
既然前面提到了 Java 虚拟机的数据类型,在此再简单介绍一下它们。一个 Slot 可以存放一个 32 位以内的数据类型,Java 中占用 32 位以内的数据类型有 boolean、byte、char、short、int、float、reference 和 returnAddress 8 种类型。前面 6 种不需要多加解释,读者可以按照 Java 语言中对应数据类型的概念去理解它们(仅是这样理解而已,Java 语言与 Java 虚拟机中的基本数据类型是存在本质差别的),而第 7 种 reference 类型表示对一个对象实例的引用,虚拟机规范既没有说明他的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在 Java 堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现 Java 语言规范中定义的语法约束约束。第 8 种即 returnAddress 类型目前已经很少见了,它是为字节码指令 jsr、jsr_w 和 ret 服务的,指向了一条字节码指令的地址,很古老的 Java 虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。
对于 64 位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。Java 语言中明确的(reference 类型则可能是 32 位也可能是 64 位)64 位的数据类型只有 long 和 double 两种。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始至局部变量表最大的 Slot 数量。如果访问的是 32 位数据类型的变量,索引 n 就代表了使用第 n 个 Slot,如果是 64 位数据类型的变量,则说明会同时使用 n 和 n+1 两个 Slot。对于两个相邻的共同存放一个 64 位数据的两个 Slot,不允许采用任何方式单独访问其中的某一个,Java 虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非 static 的方法),那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字 “this” 来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。
为了尽可能节省栈帧空间,局部变量中的 Slot 是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超出了某个变量的作用域,那这个变量对应的 Slot 就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot 的复用会直接影响到系统的垃圾收集行为,首先看如下代码
public static void main(String[] args) { byte[] placeholder = new byte[64 * 1024 * 1024]; System.gc(); } 复制代码
如上代码很简单,即申请了了 64 MB 的数据,然后通知虚拟机进行垃圾收集。我们在虚拟机运行参数中加上“-verbose:gc” 来看看垃圾收集的过程,发现在 System.gc() 运行后并没有回收这 64 MB 的内存。 没有回收 placeholder 所占的内存能说得过去,因为在执行 System.gc() 时,变量 placeholder 还处于作用域之内,虚拟机自然不敢回收 placeholder 的内存。那我们把代码修改如下:
public static void main(String[] args) { { byte[] placeholder = new byte[64 * 1024 * 1024]; } System.gc();}复制代码
加入了花括号之后,placeholder 的作用域被限制在花括号之内,从代码逻辑上讲,在执行 System.gc() 的时候,placeholder 已经不可能再被访问了,但执行一下这段程序,会发现运行结果如下,还是有 64MB 的内存没有被回收,这又是为什么呢? 在解释为什么之前,我们先对这段代码进行第二次修改如下:
public static void main(String[] args) { { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; System.gc();}复制代码
这个修改看起来很莫名其妙,但运行一下程序,却发现这次内存真的被正确回收了。
在如上代码中,placeholder 能否被回收的根本原因是:局部变量中的 Slot 是否还存在关于 placeholder 数组对象的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,没有任何局部变量表的读写操作,placeholder 原本占用的 Slot 还没有被其他变量所复用,所以作为GCRoots一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量的内存、实际上已经不会再使用的变量,手动将其设置为 null 值(用来代替那句 int a=0,把变量对应的局部变量表 Slot 清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到 JIT 的编译条件)下的 “奇技” 来使用。
Java 语言的一本著名书籍《Practical Java》中把 “不使用的对象应手动赋值为 null” 作为一条推荐的编码规则。
关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在 “准备阶段”。通过之前的 讲解,我们已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始化;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为 Java 中任何情况下都存在诸如整型变量默认为 0,布尔型变量默认为 false 等这样的默认值。先来看看下面的代码:
public static void main(String[] args) { int a; System.out.println(a);}复制代码
这段代码其实并不能运行,还好编译器能在编译期间就检查到并提示这一点,即便编译能通过或者手动生成字节码的方式制造出下面 代码的效果,字节码校验的时候也会被虚拟机发现而导致类加载失败。
注:注意占用较大内存局部变量的gc
-
本地方法栈
本地方法,即为native方法,在看jdk源码的时候,可以经常看到,像Object的getClass方法、hashCode方法等都是native方法,这些方法不是用Java实现的,本地方法本质上是依赖于实现的,虚拟机实现的设计者们可以自由地决定使用怎样的机制来让Java程序调用本地方法。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。
很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。
下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。这幅图展示了Java虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
本地方法栈与虚拟机栈其实很类似,只是虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机执行本地方法服务。
java中有两种方法: java方法和本地方法。
java方法是有java语言编写,编译成字节码,存储在class文件中的。本地方法是有其它语言(比如C,C++,或者是会变语言)编写的,编译成和处理器相关的机器代码。本地方法保存在动态连接库中,格式是各个平台专用的。java方法是与平台无关的,但是在本地方法却不是。运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
-
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。在jdk1.4中加入了NIO类,引入了一种基于通道(Channel)于缓冲区(Buffer)的I/O方式,他可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆中和native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括ram及swap区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。