您的位置:首页 > 其它

JVM_1_运行时内存区域

2017-09-15 22:12 106 查看
Java内存区域

内存区域这篇文章的篇幅会比较长,因为之前看过一遍,如果不将内存各个区域做什么的了解清楚,后面看的会很累。
所以网上搜罗了一些讲解的比较好的文章,放着多理解理解...

我将Java虚拟机规范中文版上传了,点击下面链接,即可下载
Java虚拟机规范SE7中文版下载

运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:

 
(原书的图黑不溜秋的..我这里从网上找的图片...)



 
我们先看看上图灰色的部分:运行时数据区。图中很细心的帮我们做好了区分。
运行时数据区按照线程共享与否,可以划分为:线程共享线程私有。
线程共享:方法区、堆
线程私有:Java栈(虚拟机栈)、本地方法栈、程序计数器

 
下面我们按个来了解下,这些内存区域是做什么的....

 
注意:堆是堆(heap),栈是栈(stack),堆栈是栈。


Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是线程共享的,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
在Java虚拟机规范中是这样描述的:所有的对象实例以及数组都要在堆上分配
随着技术发展,所有对象都在堆上分配也渐渐变得不那么"绝对"了。
 
Java堆是垃圾搜集器管理的主要区域,因此很多时候也被称作"GC堆"。
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。

 
(原书中堆介绍的比较细致,也比较杂,我这里只挑重点,以下是网上找的资料)

堆中内置了"自动内存管理系统",也就是我们常说的"垃圾搜集器(GC)"。
堆中内存的释放是不受开发人员的控制的,完全由Java虚拟机一手操作。

堆的容量可以使固定的,也可以随着需要进行扩展,并且在不用的时候自动收缩。
简单地说,堆容量是"弹性的"

 

-------------------------------------------------------------------------------------------------------------------



上图,对象存储在内存区域中,在JVM中,那个内存区域就是堆~
从上图可以看出,堆的结构和栈是不同的,堆在内存中并不是一块连续的区域,它是分散的(物理上分散,逻辑上连续);
虚拟机通过栈中引用的指引在堆中找到所需要的对象。
 
在虚拟机遇到一条new指令的时候,经过一系列操作之后(暂时不深究),虚拟机就要为该新生对象分配内存空间了。
 

-------------------------------------------------------------------------------------------------------------------

《Java虚拟机规范 Java SE 7》一书中的说明:

在Java虚拟机中,(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。

Java堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统(也就是"GC垃圾搜集器")所管理的对象。(后半句真拗口╮(╯▽╰)╭)

Java堆的容量是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。

Java堆所使用的内存不需要保证是连续的。

方法区 

方法区也是个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

方法区是堆的逻辑组成部分,它有一个别名叫做:Non-Heap(非堆)。

方法区在程序运行时被创建。

方法区存储了每一个类的结构信息,比如常量池,字段和方法数据、构造函数和普通方法的字节码内容。

它与Java堆的区别除了存储的信息与Java堆不一样之外,最大的区别是这一部分Java虚拟机不强制要求实现自动内存管理系统(GC)

JDK1.7开始,将原本放在"永久代"中的字符串常量池移出。(原书说的,网上有争议)

很多人将HotSpot虚拟机中方法区称为"永久代"。

对于在HotSpot虚拟机上开发的开发者来说,很多人更愿意将方法区称为"永久代",本质上两者并不等价......
(这一段是书中的原文,对此很不理解,方法区和永久代到底啥关系呢?) 

-------------------------------------------------------------------------------------------------------------------

好,那么问题来了,常说的永久代和方法区是什么关系呢?

整理下网上找的资料:

永久代是HotSpot虚拟机特有的概念,永久代是方法区的一种实现

平时说到永久代(PermGen space)的时候往往将其和方法区不加以区别。这么理解在一定角度也是说的过去的。
因为在《Java虚拟机规范》中,只是规定了有方法区这么一个概念和它的作用,并没有规定如何去实现它
那么在不同的JVM上方法区的实现肯定都是不一样的。
大多数的JVM使用的都是Sun公司的Hotspot,在Hotspot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。

虽然可以牵强的解释这种将方法区和永久代等同对待的观点,但最终方法区和永久代还是不同的
一个是标准,一个是实现,这就相当于你将Java中的接口和接口的实现等同对待一样。
同时,这种牵强的解释也仅仅在Hotspot虚拟机上才勉强成立,其他的虚拟机没有永久代这一说法。

公司同事给出的解释,与网上找的差不多:
方法区是java虚拟机规范中的说法,这个规范大概意思是,如果你想实现java虚拟机 需要包含这几个数据区域
永久代是实现了方法区的内存管理,可以这么理解:方法区是数据,永久代就是存放这个数据的内存区域(最后一句我保留意见)

------------------------------------------------------------------------------------------------------------------ 

《Java虚拟机规范
Java SE 7》一书中的说明:

在Java虚拟机中,方法区是可供各条线程共享的运行时内存区域。

方法区与传统语言中的编译代码存储区或者操作系统进程的正文段的作用非常类似,它存储了每一个类的结构信息,例如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容、还包括一些类、实例、接口初始化时用到的特殊方法。

方法区在虚拟机启动的时候创建,虽然方法去是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾搜集。
(不强制虚拟机实现方法区的垃圾搜集)

方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。

程序计数器

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

在虚拟机概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会

处理一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间

计数器互不影响,独立存储。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

-------------------------------------------------------------------------------------------------------------------

《Java虚拟机规范
Java SE 7》一书中的说明:

Java虚拟机可以支持多线程同时执行,每一条Java虚拟机线程都有自己的PC寄存器(程序计数器)。
在任意时刻,一条Java虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法被称为该线程的当前方法。

如果这个方法不是native的,那PC寄存器就保存Java虚拟机正在执行的字节码指令的地址;
如果该方法时native的,那PC寄存器的值是undefined。
PC寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个平台相关的本地指针的值。

Java栈(Java虚拟机栈)
Java栈是线程私有的,它的生命周期与线程相同。
Java栈描述的是Java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧(StackFrame)用于存储局部变量、操作数栈、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中--入栈到出栈的过程。

JVM规范让每一个Java线程都拥有自己独立的Java栈,也就是Java方法的调用栈。

(好吧,上面说了一堆,我还是没怎么理解....)

-------------------------------------------------------------------------------------------------------------------

栈是我们最常用的内存区域。它主要是用来存放基本类型变量局部变量以及对象的引用
举个栗子:
Useruser = new User();
这里的user就是对象的引用,也可以理解为地址,指引着虚拟机要去哪里找user这个对象。如下图:



由图可知,当我们将一个对象作为方法的参数时,在方法中修改这个对象的值,也会影响到原来的对象,因为我们只是改变了图中内存区域的值,它的指引(地址)还是一样的。

同时可以看出,栈的内存区域是连续的,有大小限制的,如果超过了就会抛出栈溢出的异常StackOverflowError。

每个方法执行的时候,都会创建一个个栈帧,用于保存局部变量,操作数栈、动态链表等信息。

每次方法的调用都会对应着一个栈帧,因此可以解释有我们在写递归程序的时候不小心报栈溢出的异常,因为栈时有限的,方法调用太多次导致栈帧存满,所以溢出。

注意:局部变量表操作栈...都是存储在栈帧里面的。

是不是似乎有些明朗了?但还是有些云里雾里??

-------------------------------------------------------------------------------------------------------------------

《Java虚拟机规范
Java SE 7》一书中的说明:

每一条Java虚拟机线程都有自己私有的Java虚拟机栈,这个栈与线程同时创建,用于存储栈帧。

Java虚拟机栈的作用与传统语言(C语言)中的栈非常类似,就是用于存储局部变量与一些过程结果的地方。

另外,它在方法调用和返回中也扮演了很重要的角色。

因为除了栈帧的出栈和入栈之外,Java虚拟机栈不会受其他因素的影响,所以栈帧可以在堆中分配,Java虚拟机所使用的内存不需要保证是连续的。

看到这里,是不有问题了?这个栈帧是什么??

这里不在拓展,我们下面单独讲....

本地方法栈

本地方法栈与Java栈(Java虚拟机栈)所发挥的作用是非常相似的,它们的区别不过是:

虚拟机栈为虚拟机执行Java方法(字节码)服务。

本地方法栈为虚拟机使用到的本地(Native)方法服务

在Hotspot JVM中 
Java虚拟机栈与本地方法栈是合二为一的。

-------------------------------------------------------------------------------------------------------------------

《Java虚拟机规范
Java SE 7》一书中的说明:

Java虚拟机实现可能会使用到传统的栈(stacks)来支持native方法的执行,这个栈就是本地方法栈。

如果Java 虚拟机不支持 natvie 方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈;

如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配

拓展

运行时常量池

运行时常量池并没有在上图中体现出来,不过这个内存区域还是很重要的。

还记不记得,上面讲方法区的时候,我们了解过,方法区和永久代的概念?

在Hotspot虚拟机中,永久代是方法区的实现~

在JDK1.7之后,常量池移出了永久代~

运行时常量池是方法区的一部分。

Class文件中除了有类的版本、字段、方法、接口等描述信息之外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容

运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java预研并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的是String类的intern()方法。

既然运行时常量池是方法区的一部分,自然受到方法区内存限制,当常量池无法再申请到内存时会抛出OOM异常。

------------------------------------------------------------------------------------------------------------------------

《Java虚拟机规范
Java SE 7》一书中的说明:

运行时常量池是每一个类或接口的常量池的运行时表示形式。

它包含了若干种不同的常量:从编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。

每一个运行时常量池都分配在Java虚拟机的方法区(Hotspot永久代)之中,在类和接口被加载到虚拟机之后,对应的运行时常量池就被创建出来了。

栈帧
《Java虚拟机规范
Java SE 7》一书中的说明:

栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成都算作方法结束。

栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类运行时常量池的引用。

在一条线程中,只有目前正在执行的那个方法的栈帧是活动的。

这个栈帧就被称作是当前栈帧,这个栈帧对应的方法就被称为是当前方法,定义这个方法的类被称作当前类。

对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的局部变量表和操作数栈进行的操作。

需要特别注意的是:
栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另一条线程的栈帧。

局部变量表
《Java虚拟机规范
Java SE 7》一书中的说明:
每个栈帧内部都包含一组称为局部变量表的变量列表。
栈帧中局部变量的长度由编译器决定,并且存储于类和接口的二进制中,通过Code方法的属性保存以及提供给栈帧使用。

一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个Long和double的数据

Java虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至0开始的连续的局部变量表位置上(后半句没太看明白

操作数栈
《Java虚拟机规范
Java SE 7》一书中的说明:
每一个栈帧内部都包含一个称为操作数栈的后进先出栈。
栈帧中操作栈的长度由编译器决定,并且存储于类和接口的二进制表之中。

操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。
Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈之中。(看的有些头疼..)

在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。

引用类型与值
《Java虚拟机规范
Java SE 7》一书中的说明:
Java虚拟机中有三种应用类型:类类型、数组类型、接口类型
这些引用类型的值分别由类实例、数组实例、实现了某个接口的类实例或数组实例动态创建。

在引用类型的值中还有一个特殊的值:null;
当一个引用不指向任何对象的时候,它的值就用null来表示
一个null的引用,在没有上下文的情况下不具备任何实际的类型,但是有具体上下文时它可转型为任意的引用类型。引用类型默认值就是null。

JVM中的堆、栈该如何去理解

 
相信有些童鞋可能会对堆、栈的概念比较模糊..讲真,我自己也有些模糊,觉得自己似乎明白了,但是要我说给别人听,似乎又有点讲不出...

 
我们先复习下堆、栈的定义

堆:线程共享、所有对象实例以及数组都在堆上分配、物理空间不连续,逻辑连续.
栈:线程私有、每个方法执行时都会创建一个栈帧、存放基本类型、局部变量、对象引用

网上找了些资料,加深下理解...

-------------------------------------------------------------------------------------------------------------------------------

堆中分配的是对象,也就是new出来的东西。
栈中分配的是基本类型和自定义对象的引用。

我们举个栗子:



-------------------------------------------------------------------------------------------------------------------------------

换种角度给大家一个直观的印象,说白了就是stack和
heap这两个词。
用这两个词在google中搜索图片,可以得到下面一些结果

Stack



Heap



参考资料:
《Java Virtual Machine Specification Java SE 7 中文版》(Java虚拟机规范 Java SE7)
《深入理解Java虚拟机——JVM高级特性与最佳实践》
《JVM进阶(一)——初识JAVA栈》
《JVM进阶(二)——初识JAVA堆》
《Java虚拟机的堆、栈、堆栈如何去理解?》
《JVM基础:深入学习JVM堆与JVM栈》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: