深入理解 Java 虚拟机 (一) JVM 内存区域

一、概述

对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。

二、运行时数据区域

Java 虚拟机在执行 Java 程序的时候会把它管理的内存区域划分成若干个不同的数据区域,每个区域都有各自的用途,有些区域里的数据在 JVM 启动的时候创建,退出的时候销毁。有些数据在线程创建时创建,在线程退出时销毁。JVM 所管理的内存区域主要有以下几个运行时数据区域。如图:
Java 1.7

1.程序计数器(线程私有)

程序计数器是一块较小的内存空间,可以把它看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

程序计数器是线程私有的,由于多线程采用的是时间片轮转机制,所以程序计数器保证了,在多线程切换后还能正确执行到当前线程的执行位置,使各个线程之间互不影响。

如果线程正在执行的是 Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址,如果线程执行的是 Native 方法,则程序计数器的值为空(Undefined)。==此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

2.Java 虚拟机栈(线程私有)

Java 虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同事都会创建一个栈帧,用于存储局部变量表,操组数栈,动态链接,方法出口信息等。每一个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表中存放了编译期可知的八种数据类型(byte,short,int,long,boolean,char,float,double)、对象引用(reference类型,不同于对象本身,可能就是一个指向对象起始地址的引用指针)和 returnAddress类型(指向了一条字节码指令的地址)。

局部变量表所需要的内存空间在编译期间就已完成分配,当进入到一个方法时,这个方法在需要的栈帧中分配多大的局部变量空间是完全确定的,在方法的运行期间是不会修改局部变量表的大小的。

在 Java 虚拟机规范中,对虚拟机栈规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度会抛出 StackOverFlowError 异常;如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,就会抛出 OutOFMemoryError 异常。

3.本地方法栈(线程私有)

本地方法栈与 Java 虚拟机栈的作用其实是相似的,它们之间的唯一区别是 Java 虚拟机栈是执行 Java 方法服务,而本地方法执行的是 Native 方法服务。在 Sun 公司的 Hotspot 虚拟机中也是将本地方法栈和 Java 虚拟机栈合二为一。本地方法栈也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

4.Java 堆(线程共享

对于大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块。Java 堆是所以线程共享 的区域,在虚拟机启动是创建。此内存区域的唯一目的就是存放对象的实例。几乎所有的对象实例都是在这里分配的。注意这里是几乎,而不是全部,意思就是有些情况下对象也是不会再堆中分配的,例如: 逃逸分析,JIT技术,标量替换等。

Java 堆是垃圾回收的主要区域,也被称为 GC堆,关于垃圾收集在后面的文章中我也会详细的去介绍一下,这里只做一些基本概念的了解。
根据垃圾收集的较多我们队堆的划分可以分为 新生代、老年代,比较细致一点的对新生代还可以划分 Eden 区和 From Survivor(Survivor0)和 To Survivor(Survivor1)区等。

  • 正常内存分配比例如下,可通过 JVM 参数修改:
		老年代 : 2/3的堆空间
		年轻代 : 1/3的堆空间
			Eden 区: 8/10 的年轻代空间
			 From Survivor(survivor0) : 1/10 的年轻代空间
			 To Survivor(survivor1) : 1/10 的年轻代空间

5.方法区(线程共享,JDK 1.8 已被元空间替代

方法区和 Java 堆一样,是线程共享的内存区域,主要用于存储已被虚拟机加载的类信息常量静态变量即时编译后的代码等数据。虽然 Java 虚拟机规范中将方法区描述成堆的一个逻辑部分,但是它还有一个别名叫做No-Heap(非堆)又称永久代,目的就是为了与 Java 堆区分开来。

在 JDK1.8 之后,该内存区域已经完全被元空间替代。其实移除方法区即(永久代)的工作在 JDK1.7 就已经开始了,存储永久代的部分数据就已经被转移到了 Java 堆和 Native 堆中了,例如:符号引用(Symbols)转移到了 Native Heap字面量(interned strings)转移到了Java Heap类的静态变量(class statics)转移到了 Java Heap字符串常量转移到了 Java Heap。但永久代仍存在于JDK1.7中,并没有被完全移除。

元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:-XX:MetaspaceSize,-XX:MaxMetaspaceSize,在 JDK1.8 中不会再出现永久代溢出,而会出现元空间溢出。

问:为什么 JVM 要将永久代转移到元空间?
答: 1.字符串存在永久代中,容易出现性能问题和内存溢出;
2.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出;
3.永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

6.直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存会被频繁的使用。

直接内存避免了在 Java 堆和 Native 堆中来回复制数据。直接内存的分配不会受到 Java 堆大小的限制,但会受到本机总内存大小限制。所以在配置虚拟机参数时,不能忽略直接内存以防止出现 OutOfMemoryError 异常。

直接内存适合有很大的数据需要存储的场景,它的生命周期很长,适合频繁的IO操作,例如:网络并发场景等。

三、总结

以上便是 JVM 的内存区域介绍,这里我们需要重点关注的是 Java 堆 和 方法区的区域,这里也是我们后面将要介绍到的类加载、垃圾回收(GC)以及 JVM 内存调优的主要区域,所以了解 JVM 的内存区域只是学习 Java 虚拟机路程中的一小步而已,关于类加载、垃圾回收以及内存调优才是我们需要掌握的重点。别着急,万丈高楼平地起,毕竟在之后的开发之路上肯定是会遇到各种内存问题的。遇见的多了,自然掌握就不再是难事了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值