最近在看《 JAVA并发编程实践 》这本书,里面涉及到了 Java 内存模型,通过 Java 内存模型顺理成章的来到的 JVM 内存结构,关于 JVM 内存结构的认知还停留在上大学那会的课堂上,一直没有系统的学习这一块的知识,所以这一次我把《 深入理解Java虚拟机JVM高级特性与最佳实践 》、《 Java虚拟机规范 Java SE 8版 》这两本书中关于 JVM 内存结构的部分都看了一遍,算是对 JVM 内存结构有了新的认识。JVM 内存结构是指:Java 虚拟机定义了若干种程序运行期间会使用的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,另一些则与线程一一对应,随着线程的开始而创建,随着线程的结束而销毁。具体的运行时数据区如下图所示:

在 Java 虚拟机规范中,定义了五种运行时数据区,分别是 Java 堆、方法区、虚拟机栈、本地方法区、程序计数器,其中 Java 堆和方法区是线程共享的。接下来就具体看看这 五种运行时数据区。

Java 堆(Heap)

Java 堆是所有线程共享的一块内存区域,它在虚拟机启动时 就会被创建,并且单个 JVM 进程有且仅有一个 Java 堆。Java 堆是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里。所以这里也就成为了垃圾回收器的主要活动营地了,于是它就有了一个别名叫做 GC 堆,根据垃圾回收器的规则,我们可以对 Java 堆进行进一步的划分,具体 Java 堆内存结构如下图所示:

我们可以将 Java 堆划分为新生代和老年代两个大模块,在新生代中,我们又可以进一步分为 Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1),Survivor 空间有一个为空,用于发生 GC 时存放存活对象,老年代存放的是经过多次 Minor GC 仍然存活的对象或者是一些大对象,FGC 就是发生在老年代。

上面就是 Java 堆的具体结构,我们也知道 Java 堆中的各空间大小,我们是可以动态控制的,这个在图中我也进行了简单的标注,下面我们一起来详细的了解一下这三个参数:

  • -Xms:JVM启动时申请的初始Heap值,默认为操作系统物理内存的1/64,例如-Xms20m
  • -Xmx:JVM可申请的最大Heap值,默认值为物理内存的1/4,例如-Xmx20m,我们最好将 -Xms 和 -Xmx 设为相同值,避免每次垃圾回收完成后JVM重新分配内存;
  • -Xmn:设置新生代的内存大小,-Xmn 是将NewSize与MaxNewSize设为一致,我们也可以分别设置这两个参数

在 Java 堆中会发生 OOM 异常,当我们的 Java 堆内有足够的空间去完成实例分配时,并且堆也无法扩展,将会抛出我们常见的OutOfMemoryError 异常,如下图所示:

关于 OOM 异常,我还是想多说一句,网上有一道非常火的面试题:JVM 堆内存溢出后,其他线程是否可继续工作?,我个人觉得不少回答是错误的,有兴趣的可以研究一下。

方法区(Method Area)

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,是 Java 虚拟机中唯二的内存共享区域。在 Java 虚拟机规范中是这样定义方法区的:它存储了每个类的结构信息,例如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。

方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集与压缩,方法区在实际内存空间中可以不是连续的,对于方法区的容量,你可以是固定的,也可以随着程序的执行动态扩展,并且在不需要过多空间时自动收缩。

上面都是 Java 虚拟机中的规范,来看看具体的实现,拿我们常用的 HotSpot 虚拟机来说,在 JDK1.8 之前,方法区也被称作为永久代,这个方法区会发生我们常见的 java.lang.OutOfMemoryError: PermGen space 异常,我们也可以通过启动参数来控制方法区的大小:

  • -XX:PermSize 设置最小空间
  • -XX:MaxPermSize 设置最大空间

在 JDK1.8 之后,HotSpot 虚拟机对方法区进行了不小的改动,彻底移除了永久代,将原来存放在永久代的数据迁移至 Java 堆 或者 Metaspace,方法区被移至到了 Metaspace,字符串常量移至 Java Heap,换句话说就是 JDK1.8 开始,Metaspace 也就是我们所谓的方法区,为什么要做这个改变呢?也许是基于以下两点原因:

  • 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM
  • 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。

我们也可以通过设置参数来控制 Metaspace 的空间大小,主要有以下几个命令:

  • -XX:MetaspaceSize :分配给类元数据空间(以字节计)的初始大小。MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
  • -XX:MaxMetaspaceSize: 分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
  • -XX:MinMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。
  • -XX:MaxMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。

Java 虚拟机栈(JVM Stacks)

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

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

Java 虚拟机栈既允许被实现成固定的大小,也允许根据计算动态来扩展和收缩,如果采用固定大小的话,每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。在 Java 虚拟机栈中会发生两种异常,这个在虚拟机规范中有指出:

  • 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常;
  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的 Java 虚拟机栈,那么虚拟机将会抛出 OutOfMemoryError 异常。

程序计数器(Program Counter Register)

程序计数器也是线程私有的,它只需要一块较小的内存空间,你可以把它看作当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

我们知道在多线程的情况下,并不是一条线程一直执行完,而是多个线程轮流切换执行,所以为了线程切换后能够恢复到正确的执行位置,我们就需要程序计数器来告诉线程接下来该执行哪条指令。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是 Natvie 方法,这个计数器值则为空(Undefined)。

需要特别注意的是,程序计数器是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域

本地方法栈(Native Method Stacks)

本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是 Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

与 Java 虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

参考

  • 《 深入理解Java虚拟机JVM高级特性与最佳实践 》
  • 《 Java虚拟机规范 Java SE 8版 》

最后

目前互联网上很多大佬都有 JVM 内存结构相关文章,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢,欢迎扫码关注微信公众号:「平头哥的技术博文」,和平头哥一起学习,一起进步。

这一次,终于系统的学习了 JVM 内存结构的更多相关文章

  1. jvm学习006 jvm内存结构分配

    主要内容如下: JVM启动流程 JVM基本结构 内存模型 编译和解释运行的概念 一.JVM启动流程: JVM启动时,是由java命令/javaw命令来启动的. 二.JVM基本结构: JVM基本结构图: ...

  2. JVM学习01:内存结构

    JVM学习01:内存结构 写在前面:本系列分享主要参考资料是  周志明老师的<深入理解Java虚拟机>第二版. 内存结构知识要点Xmind梳理 案例分析 分析1 package com.h ...

  3. java的线程安全、单例模式、JVM内存结构等知识学习和整理

    知其然,不知其所以然 !在技术的海洋里,前路漫漫,我一直在迷失着自我. 欢迎访问我的csdn博客,我们一同成长! "不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!" 博 ...

  4. 学习记录--JVM内存分析

    今天刷牛客网的题时,看到了两位大神关于JVM内存分析的讲解,就顺手记录学习一下. JVM内存模型图 方法区域存放了所加载的类的信息(名称.修饰符等).类中的静态变量.类中定义为final类型的常量.类 ...

  5. 2万字长文包教包会 JVM 内存结构 保姆级学习笔记

    写这篇的主要原因呢,就是为了能在简历上写个"熟悉JVM底层结构",另一个原因就是能让读我文章的大家也写上这句话,真是个助人为乐的帅小伙....嗯,不单单只是面向面试学习哈,更重要的 ...

  6. JVM内存结构---《深入理解Java虚拟机》学习总结

    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域的用途各不相同,同时也依据着各自的执行规则,独立的创建和销毁数据. 虚拟机内存的划分,如图所示: 线程之间 ...

  7. JVM学习心得—JVM内存模型(个人整理,请勿转载)

    一.运行时数据区域 线程私有的:程序计数器+虚拟机栈+本地方法栈 线程共享的:堆+方法区(运行时常量池)+直接内存(非运行时数据区的一部分) *JDK1.8后将方法区废除,新增元空间. 1.1 程序计 ...

  8. JVM学习十二 - (复习)JVM内存结构

    JVM 内存结构 Java 虚拟机的内存空间分为 5 个部分: 程序计数器 Java 虚拟机栈 本地方法栈 堆 方法区 JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代.元 ...

  9. 如何系统的学习Java

    初学者记住一点,学习Java一定是连续性的且循序渐进的“系统化”学习,首先我给你提供一个优秀Java工程师的学习路线. web前端方面:html.css,Java.jQuery.xml解析.Boots ...

随机推荐

  1. 【ADO.NET基础】后台获取前台控件

    C# 后台获取前台 input 文本框值.string aa=Request.Form[headself]; 那么要是后台给前台input文本框赋值呢? 后台 public string Headse ...

  2. Angular template ng-template/container/content

    1. ng-template 形式:<ng-template>...</ng-template> 默认ng-template中的内容会隐藏; 可通过[ngIf]来控制内容显示隐 ...

  3. openstack问题记录

    先去查看对应的日志:/var/log/,再来排查错误 1.实例处于错误状态 解决办法: 1.使用openstack hypervisor list查看 2.然后openstack hypervisor ...

  4. 002-python函数、高级特性

    1.函数 1.1 定义函数 在Python中,定义一个函数要使用def语句,依次写出函数名.括号.括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回 自定义一个求绝对 ...

  5. 基于vue-cli、elementUI的Vue超简单入门小例子

    - 这个例子还是比较简单的,独立完成后,能大概知道vue是干嘛的,可以写个todoList的小例子. - 开始写例子之前,先对环境的部署做点简单的介绍,其实和Vue官方的差不多. #如若没有安装过vu ...

  6. SpringBoot导入jsp依赖始终报错

    先粘出我自己的pom代码: <?xml version="1.0" encoding="UTF-8"?> <project xmlns=&qu ...

  7. 六、springboot 简单优雅是实现短信服务

    前言 上一篇讲了 springboot 集成邮件服务,接下来让我们一起学习下springboot项目中怎么使用短信服务吧. 项目中的短信服务基本上上都会用到,简单的注册验证码,消息通知等等都会用到.所 ...

  8. 分享一个移动端rem布局的适配mixin

    /*================================================================ 以下为基于ip5 宽度320做的适配,标准html{font-si ...

  9. 数据表管理admin

    知识预览 admin组件使用 admin源码解析 回到顶部 admin组件使用 Django 提供了基于 web 的管理工具. Django 自动管理工具是 django.contrib 的一部分.你 ...

  10. 使用低版本的VS打开高版本项目的解决方案(以VS2008打开VS2010开发的项目为例)

    使用低版本的VS打开高版本项目的解决方案,这里以VS2008打开VS2010开发的项目为例. 右键项目的sln文件以记事本的方式打开: 将对应的前两列版本各降到对应的版本,这边的11.00改为10.0 ...