JVM 内存划分

JVM 内存划分

堆 & 栈是两块不同的内存区域,简单理解:堆是用来存放对象栈是用来执行程序的。当然这种划分方式比较粗糙,只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块,Java 内存区域的划分实际上远比这复杂。

一、运行时的区域

Java 虚拟机(JVM)内部定义了程序运行时需要使用到的内存区域。

每个区域都有自己的用途,创建和销毁的时间。图中绿色部分就是所有线程之间共享的内存区域。而其余部分则是线程运行时独有的数据区域。

1.1、线程独有的内存区域

1.1.1、PROGRAM COUNTER REGISTER 程序计数器

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
在 JVM 规范中规定,如果线程执行的是非 native 方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是 native 方法,则程序计数器中的值是 undefined。
因为程序计数器中存储的数据所占的空间大小不会随程序的执行而发生改变。因此不会发生内存溢出 OutOfMemory现象的。
总结:当前线程所执行的字节码的行号指示器;当前线程私有;不会出现 OutOfMemoryError 情况。

1.1.2、JAVA STACK 虚拟机栈

Java 栈也称作虚拟机栈(Java Vitual Machine Stack),Java 栈是 Java 方法执行的内存模型。
Java 栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表 (Local Variables)操作数栈 (Operand Stack)、指向当前方法所属的类的运行时常量池的引用 (Reference to runtime constant pool)方法返回地址 (Return Address)一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。
当方法执行完毕后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于 Java 栈的顶部。这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个 Java 栈的模型:

  • 局部变量表:用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
  • 操作数栈:栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。程序中的所有计算过程都是在借助于操作数栈来完成的。
  • 指向运行时常量池的引用:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
  • 方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
  • 由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的 Java 栈,互不干扰。

每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体 JVM 的实现有关,通常在 256K~756K 之间。

1.1.3、NATIVE METHOD STACK 本地方法栈

本地方法栈与 Java 栈的作用和原理非常相似。区别只不过是 Java 栈是为执行 Java 方法服务的,而本地方法栈则是为执行 本地方法 Native Method 服务的。在 JVM 规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在 HotSopt 虚拟机中直接就把本地方法栈和 Java 栈合二为一。

1.2、线程间共享的内存区域

1.2.1、HEAP 堆

大多数应用,堆都是 Java 虚拟机所管理的内存中最大的一块,它在虚拟机启动时创建,此内存唯一的目的就是存放对象实例。由于现在垃圾收集器采用的基本都是分代收集算法所以堆还可以细分为新生代和老年代,再细致一点还有 Eden 区、From Survivior 区、To Survivor 区。
总结:1、可以通过 -Xmx-Xms 控制堆的大小;2、OutOfMemoryError 异常:当在堆中没有内存完成实例分配,且堆也无法再扩展时。

1.2.2、METHOD AREA 方法区

方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虚拟机规范是把这块区域描述为堆的一个逻辑部分的,但实际它应该是要和堆区分开的。从分代收集算法角度看,HotSpot 中,方法区≈永久代。不过 JDK 7 之后,使用的 HotSpot 应该就没有永久代这个概念了,会采用 Native Memory 来实现方法区的规划了。

1.2.3、RUNNING CONSTANT POOL 运行时常量池

上面的图中没有画出来,因为它是方法区的一部分。Class 文件中除了有类的版本信息、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,另外翻译出来的直接引用也会存储在这个区域中。这个区域另外一个特点就是动态性,Java 并不要求常量就一定要在编译期间才能产生,运行期间也可以在这个区域放入新的内容,String.intern () 方法就是这个特性的应用。

1.3、直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致内存溢出问题。JDK1.4 中新增加了 NIO,引入了一种基于通道与缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM、SWAP 区)大小以及处理器寻址空间的限制。
总结:
1、NIO 可以使用 Native 函数库直接分配堆外内存,堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作;
2、大小不受 Java 堆大小的限制,受本机 (服务器) 内存限制;
3、OutOfMemoryError 异常:系统内存不足时。

二、对象的创建

Java 是一门面向对象的语言。Java 程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(克隆、反序列化)就是一个 new 关键字而已,但是虚拟机层面上却不是如此。看一下在虚拟机层面上创建对象的步骤:

  • 1、虚拟机遇到一条 new 指令,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么必须先执行类的初始化过程。
  • 2、类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从 Java 堆中划分出一块确定大小的内存而已。这个地方会有两个问题:
    • (1)如果内存是规整的,那么虚拟机将采用的是指针碰撞法来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是 Serial、ParNew 这种基于压缩算法的,虚拟机采用这种分配方式。
    • (2)如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。如果垃圾收集器选择的是 CMS 这种基于标记 - 清除算法的,虚拟机采用这种分配方式。
      另外一个问题及时保证 new 对象时候的线程安全性。因为可能出现虚拟机正在给对象 A 分配内存,指针还没有来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。虚拟机采用了 CAS 配上失败重试的方式保证更新更新操作的原子性和 TLAB 两种方式来解决这个问题。
  • 3、内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在 Java 代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 4、对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息,这些信息存放在对象的对象头中。
  • 5、执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

三、对象的访问定位

建立对象是为了使用对象,Java 程序需要通过栈上的 reference(引用)数据来操作堆上的具体对象。比如:Object obj = new Object()
new Object () 之后其实有两部分内容,一部分是类数据(比如代表类的 Class 对象)、一部分是实例数据。
由于 reference 在 Java 虚拟机规范中只是一个指向对象 new Object () 的引用 obj,并没有规定 obj 应该通过何种方式去定位、访问堆中对象的具体位置,所以对象访问方式也是取决于虚拟机而定的。主流方式有两种:

  • 1、句柄访问。java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
  • 2、指针访问。java 堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference 中存储的就是对象地址。

HotSpot 虚拟机采用的是后者,不过前者的对象访问方式也是十分常见的。

3.1、内存组成及分配

java 内存组成介绍:堆(heap)非堆(None-heap)内存
按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在 JVM 中堆之外的内存称为非堆内存 (Non-heap memory)”。
可以看出 JVM 主要管理两种类型的内存:堆和非堆。简单来说堆就是 Java 代码可及的内存,是留给开发人员使用的;非堆就是 JVM 留给自己用的,所以方法区、JVM 内部处理或优化所需的内存 (如 JIT 编译后的代码缓存)、每个类结构 (如运行时常数池、字段和方法数据) 以及方法和构造方法的代码都在非堆内存中。

  • 方法栈 & 本地方法栈:
    线程创建时产生,方法执行时生成栈帧。
  • 方法区:
    存储类的元数据信息常量等。
  • 堆:
    java 代码中所有的 new 操作
    native Memory(C heap)
    Direct Bytebuffer JNI Compile GC;

3.2、堆内存分配

JVM 初始分配的内存由 -Xms 指定,默认是物理内存的 1/64;JVM 最大分配的内存由 -Xmx 指定,默认是物理内存的 1/4。默认空余堆内存小于 40% 时,JVM 就会增大堆直到 -Xmx 的最大限制;空余堆内存大于 70% 时,JVM 会减少堆直到 -Xms 的最小限制。因此服务器一般设置 -Xms、-Xmx 相等以避免在每次 GC 后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

  • Young Generation 即图中的 Eden + From Space + To Space
  • Eden 存放新生的对象
  • Survivor Space 有两个,存放每次垃圾回收后存活的对象
  • Old Generation Tenured Generation 即图中的 Old Space 主要存放应用程序中生命周期长的存活对象

3.3、非堆内存分配

JVM 使用 -XX:PermSize 设置非堆内存初始值,默认是物理内存的 1/64;由 -XX:MaxPermSize 设置最大非堆内存的大小,默认是物理内存的 1/4。

  • Permanent Generation 保存虚拟机自己的静态 (refective) 数据主要存放加载的 Class 类级别静态对象如 class 本身,method,field 等等 permanent generation 空间不足会引发 full GC (详见 HotSpot VM GC 种类)
  • Code Cache 用于编译和保存本地代码(native code)的内存 JVM 内部处理或优化

3.4、JVM 内存限制 (最大值)

JVM 内存的最大值跟操作系统有很大的关系。简单的说就 32 位处理器虽然 可控内存空间有 4GB,但是具体的操作系统会给一个限制,这个限制一般是 2GB-3GB(一般来说 Windows 系统下为 1.5G-2G,Linux 系统 下为 2G-3G),而 64bit 以上的处理器就不会有限制了。

评论

暂无

添加新评论