JVM基础--JVM内存区域模型
Posted 豆芽花花儿酱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM基础--JVM内存区域模型相关的知识,希望对你有一定的参考价值。
一提到Java,我们第一直觉就是Java语言。其实Java不仅仅是一种编程语言,它还是由一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并且广泛应用于嵌入式系统/移动终端/企业服务器/大型机等场合。从广义上讲,类似于JRuby等运行在JVM上的语言及其相关的程序都属于Java技术体系中的一员。但是,一般咱们从传统上理解,sun官方所定义的Java技术体系包括:Java程序设计语言/各个平台上的JVM虚拟机/class文件/Java API类库/还有第三方Java类库。我们把JVM JavaAPI 和Java程序设计语言这三部分称为JDK。
简单介绍了Java之后,就进入今天的主题,jvm内存区域模型。相对于c或者c++来说,Java的自动内存机制可以每次new对象写配对的delete/free代码。不容易出现内存溢出和内存泄漏的情况。同样,有利肯定就有弊处。一旦出现内存泄漏或者内存溢出的情况,排查问题也是一项棘手的工作。
JVM在执行Java程序时,会把Java虚拟机管理的内存划分不同的数据区域。每个区域都有自己的用途。如下图所示:
这张图就表示了Java虚拟机运行时数据区。下面我们就来一一介绍着不同区域的内容。
程序计数器
程序计数器program counter register , 是一块较小的内存空间。我们可以把它当前线程所执行的字节码的行号指示器。
而在虚拟机的概念模型中,字节码解释器,是通过改变这个程序计数器的值,来选去下一条需要执行的字节码指令。一些基础功能,比如分支/循环/跳转/异常处理/线程恢复等,就是得需要这个计数器才能完成。
Java虚拟机中的多线程就是 通过线程轮流切换 并 分配处理器执行时间 的方式来实现。在任何一个时间内,一个处理器也只能执行一个线程中的指令。所以,线程之间是来回切换的,为了线程切换后能恢复到正确的执行位置。所以每条线程都配置了一个专用的独立的 程序计数器。这样,各个线程的程序计数器之间不会相互干扰,独立存储。我们称之为“线程私有”内存。
Java虚拟机栈
Java虚拟机栈 VM stack,就是执行Java方法的内存模型。这个也是线程私有的,生命周期跟线程一致。
每个Java方法被执行的时候,都会创建一个“栈帧” ,用于存储 局部变量表 和 操作数栈 和 动态链接 和局部变量区 等信息
每个Java方法从被调用 到 执行完 这个过程,就对应着一个栈帧在虚拟机栈中 从入栈 到 出栈的过程。
下图是关于一个“栈帧”的示意图:
其中值得注意的是,栈帧中的局部变量表:局部变量区所需要的内存空间,是在编译期间就完成分配的。当进入一个方法是,这个方法需要在帧中分配多大的局部变量空间是完全确定的。方法在运行期间,是不会改变局部变量表的大小。
本地方法栈
本地方法栈 native method stack ,与上面介绍的虚拟机栈基本类似。区别之处在于:上面的虚拟机栈 为执行Java方法的内存模型,而本地方法栈 是为native方法也就是操作系统提高的方法 服务的。有的虚拟机(如sun Hotspot虚拟机),就直接把本地方法栈和Java虚拟机栈合并在一起。
Java堆
java堆Java heap是Java虚拟机中内存最大的一块,是被所有线程共享的一块内存区域,随着Java虚拟机的启动而创建。
该区域的唯一目的就是 存放对象实例。所有的对象实例以及数组都是在堆上进行分配。随着JIT编译器的发展与逃逸分析技术的逐渐的发展,所有对象均在堆上进行分配也变得不那么绝对。
Java堆是 垃圾收集器管理的主要区域。因此也被称为 GC堆 : 从内存回收的角度来看,由于现在收集器基本都是采用的 分代收集算法,所以 Java堆还可以细分为:新生代Young generation 和 老年代old generation。从内存分配的角度来看,线程共享的Java堆中可能会划分出多个线程私有的分配缓冲区 也就是thread local allocation buffer ,TLAB
但是,不管是如何划分,不管是哪个区域,都是与存放的内容没有关系,存储的依旧都是对象实例,这点是不会变化的。
现在我们来介绍一下,Java堆的内存大小。JVM的大小是通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms是Java虚拟机启动时申请的最小内存,默认时操作系统物理内存的1/64,但是 大于1G,-Xmx 是Java虚拟机可以申请的最大内存,默认时操作系统的物理内存的1/4 同样也是小于1G。默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可以通过"-XX:MinHeapFreeRation=" 这个参数来制定这个比例;当空余堆内存大于70%时,JVM会减少heap的大小到-Xms指定的大小,可以通过"-XX:MaxHeapFreeRation="来指定这个比例。对于运行系统,为了避免在运行时频繁调整Heap的大小,通常把-Xms和-Xmx的值设置成一样的。
上文介绍了关于Java堆的分法,我们在这就着重介绍一下关于新生代和老年代。堆的新生代和老年代的默认比例时1:2。新生代主要时 存储新创建的对象 和 尚未进入老年代 的对象。老年代则是存储经过多次新生代GC(MinorGC)仍然存活的对象。
新生代:
-
- 程序新创新的对象都是从新生代中分配内存
- 发生在新生代中的GC 称为MinorGC
- 新生代由 Eden Space 和 两块大小相同的survivor space(一般称为S0 和 S1 或 From 和 To)来构成
- 通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation 来调整Eden Space 以及 Survivor Ration的大小,默认比例为8:1:1
老年代:
-
- 用于存储经过多次新生代GC仍然存活的对象,比如 缓存对象
- 注意:新建的对象也可能直接进入老年代:
- 大对象:通过启动参数“-XX:PretenureSizeThreshold=1024字节(默认为0)”,设置c
- 大的数组对象,且 数组中没有饮用外部对象
- 发生在老年代中的GC 称为Major GC / full GC
- 老年代所占内存大小是-Xmx减去-Xmn的值
下面是以视图的方式来展现对象在Java堆中的移动:
对象在堆中的移动:
- 绝大多数刚被创建的对象会存放在Eden区
- 在Eden区执行了第一次GC之后,存活的对象会被移动到一个Survivor空间
- 在Eden区执行GC之后,存活的对象会被堆积在同一个Survivor空间
- 当一个Survivor空间饱和之后,依旧存活的对象会被移动到另一个Survivor空间,之后会清空已经饱和的Survivor空间
- 在以上步骤重复几次后,依然存活的对象,则会被移动到老年代。一般是默认15次,但是我们可以通过选项“-XX:MaxTenuringThreshold=n” 控制经历n次GC后移动老年代
方法区
方法区 method area,其实方法区也可以称为永久代permanent generation .大家可以参考上面的图,老年代左侧有个perm区。这个就是方法区/永久代。
用于存储虚拟机加载的类信息 常量 静态变量 即时编译器编译后的代码等数据,是各个线程共享的内存区域。
默认最小内存为16MB和最大值64MB;可以通过参数 “-XX:PermSize”和“ -XX:MaxPermSize”这两个参数限制方法区的大小。
以上介绍的是JVM内存区域模型的基本部分,也是最常用的部分。我们明白了虚拟机中的内存是如何划分的,对于一些代码和操作会引起内存溢出异常,是哪一部分还需要我们继续学习和研究。虽然Java有垃圾收集机制,但是内存溢出异常也是会有的。我们常遇到的就有一些Java堆溢出 虚拟机栈和本地方法溢出 还有方法区和运行时常量池溢出等,这些都是常遇到的。但是Java中的垃圾收集机制还是会避免一些内存溢出异常的出现,读者也可以查询相关的资料进一步了解。
以上是关于JVM基础--JVM内存区域模型的主要内容,如果未能解决你的问题,请参考以下文章