JVM内存结构/运行时数据区

一、JVM运行时数据区

        从某一角度来说,Java 虚拟机的内存结构 == 运行时数据区,在《Java 虚拟机规范》中用的是【运行时数据区】术语的,并没有内存结构这么一说法。内存结构只是听着更加贴切,更加形象。

        JVM(Java Virtual Machine,Java 虚拟机)顾名思义就是用来执行 Java 程序的“虚拟主机”,实际的工作是将编译的 class 代码(字节码)翻译成底层操作系统可以运行的机器码并且进行调用执行,这也是 Java 程序能够“一次编写,到处运行”的原因(因为它会根据特定的操作系统生成对应的操作指令)。如果我们把核心类库的 API 比做数学公式的话,那么 Java 虚拟机的知识就好比公式的推导过程。掌握数学公式固然可以应付考试,但是了解背后的推导过程更加有助于记忆和理解。并且,在遇到那些没法套公式的情况下,我们也能知道如何解决。

        JVM 的种类有很多,比如 HotSpot 虚拟机,它是 Sun/OracleJDK 和 OpenJDK 中的默认 JVM,也是目前使用范围最广的 JVM。我们常说的 JVM 其实泛指的是 HotSpot 虚拟机,本文也一样。

        首先,为了有个更加直观、清晰的印象,我画了一个简单的内存结构图,里面展示了我后会提到的堆、线程栈等区域,并从说明了什么是线程私有和线程共享。

在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g8S9wT4F-1610541283851)(//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b08f2b2e88b4617b21e8f5a7e522680~tplv-k3u1fbpfcp-zoom-1.image)]

线程独享区可以中包含以下三种数据区域:

  • 程序计数器(Program Counter Register)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method Stack)

线程共享区中包含以下两种数据区域:

  • 方法区(Method Area)
  • 堆(Heap)

        在JVM外部也就是本地内存中,包含了直接内存和元数据(Metadata),在JDK 1.8中,元数据就是我们之前的永久代(持久代)

        他们之间的区别是,JDK 7在持久代中的字符串常量池在JDK 8移到了堆内存中,剩余部分(Class文件常量池、运行时常量池)移到了元数据中。

        JVM不同区域的占用内存大小不同,一般情况下堆最大,程序计数器较小。那么最大的区域会放什么?当然就是Java中最多的“对象”了。

        Java常量池有哪些:Class文件常量池、运行时常量池,当然还有全局字符串常量池,以及基本类型包装类对象常量池

二、线程私有

2.1程序计数器(PC)


        在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。

程序计数器的定义


        在JVM中,作为一种虚拟机,JVM没有指令寄存器,它是基于栈 + 程序计数器的体系结构来完成方法的执行,它可以看作是当前线程所执行的字节码的行号指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来实现

程序计数器的作用


        Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存。

程序计数器的特点


是一块较小的内存空间。线程私有,每条线程都有自己的程序计数器。生命周期:随着线程的创建而创建,随着线程的结束而销毁。是唯一一个不会出现OutOfMemoryError的内存区域。

Java虚拟机栈(Java Virtual Machine Stack)


与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型;每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。
栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。

局部变量表存放了编译期可知的各种基本数据类型、对象引用和
returnAddress 类型。

基本类型:八种基本类型

  • 对象引用:reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对
  • 象的句柄或者其他与此对象相关的位置。
  • returnAddress 类型:指向了一条字节码指令的地址。

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

在 Java 虚拟机栈中会发生两种异常,这个在虚拟机规范中有指出:

  • 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常;也就是栈溢出错误!方法递归调用产生 StackOverflowError 异常这种结果。
  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的 Java 虚拟机栈,那么虚拟机将会抛出 OutOfMemoryError 异常。也就是OOM内存溢出错误!(线程启动过多)

本地方法栈(Native Method Stack)


它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。
与 Java 虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常

三、线程共享


堆(Heap)


        Java 堆是内存空间占据的最大一块区域了,Java 堆是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里。

        对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。

        对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。我们上面提到,每个线程拥有一个虚拟机栈。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

注意,像 int[] 数组这样的内容,是在堆上分配的。数组并不是基本数据类型

在这里插入图片描述
所以这里也就成为了垃圾回收器的主要活动营地了,于是它就有了一个别名叫做 GC 堆,并且单个 JVM 进程有且仅有一个 Java 堆。根据垃圾回收器的规则,我们可以对 Java 堆进行进一步的划分,具体 Java 堆内存结构如下图所示:

在这里插入图片描述
从上图可以看出Java 堆并不是单纯的一整块区域,实际上java堆是根据对象存活时间的不同,Java 堆还被分为新生代、老年代两个区域,年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区。并且默认的虚拟机配置比例是Eden:from :to = 8:1:1 。简单来说就是:

  • Java堆 = 老年代 + 新生代
  • 新生代 = Eden + S0 + S1
  • 默认Eden:from :to = 8:1:1

        实际上我们是可以通过JVM参数动态控制 Java 堆中的各空间大小的,关于JVM的参数是有很多的,但是常用的也就那么几个,不多的,用的多了都会很容易记住的,下面我们来讲讲关于堆的JVM常见的参数:

-Xms: 堆容量初始大小(堆包括新生代和老年代)。 例如:-Xms 20M
-Xmx: 堆总共(最大)大小。 例如:-Xmx 30M
注意:建议将 -Xms 和 -Xmx 设为相同值,避免每次垃圾回收完成后JVM重新分配内存!
-Xmn: 新生代容量大小。例如:-Xmn 10M
-XX: SurvivorRatio 设置参数Eden、form和to的比例 【比例参数Eden、form和to默认是8:1:1】例如:-XX: SurvivorRatio=8 代表比例8:1:1

当我们的 Java 堆内没有足够的空间去完成实例分配时,并且堆也无法扩展,将会抛出我们常见的OutOfMemoryError 异常,也就是我们常说的OOM 异常

方法区(Method Area)


Java虚拟机规范中是这样定义方法区的:

它存储了每个类的结构信息,例如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。

        由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元空间(Metaspace)。拿HotSpot 虚拟机来说,在 JDK1.7的时候,方法区被称作为永久代, 从JDK1.8开始,Metaspace (元空间)也就是我们所谓的方法区!

        方法区(Method Area)与上面讲的Java堆一样,都是各个线程共享的,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

元空间(Meta space)


        说到这里,你应该回想一下类与对象的区别。对象是一个活生生的个体,可以参与到程序的运行中;类更像是一个模版,定义了一系列属性和操作。那么你可以设想一下。我们前面生成的 A.class,是放在 JVM 的哪个区域的?

        想要问答这个问题,就不得不提下 Java 的历史。在 Java 8 之前,这些类的信息是放在一个叫 Perm 区的内存里面的。更早版本,甚至 String.intern 相关的运行时常量池也放在这里。这个区域有大小限制,很容易造成 JVM 内存溢出,从而造成 JVM 崩溃。

        Perm 区在 Java 8 中已经被彻底废除,取而代之的是 Metaspace。原来的 Perm 区是在堆上的,现在的元空间是在非堆上的,这是背景。关于它们的对比,可以看下这张图。

在这里插入图片描述
然后,元空间的好处也是它的坏处。使用非堆可以使用操作系统的内存,JVM 不会再出现方法区的内存溢出;但是,无限制的使用会造成操作系统的死亡。所以,一般也会使用参数 -XX:MaxMetaspaceSize 来控制大小。

运行时常量池(Run-Time Constant Pool)


        这是方法区的一部分。反编译的类文件结构中你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。

OOM


        OOM 的全称是 Out Of Memory,那我们的内存区域有哪些会发生 OOM 呢?从下表中可以看到除了程序计数器,其他区域都有OOM溢出的可能。但是最常见的还是发生在堆上。
所以 OOM 到底是什么引起的呢?有几个原因:

在这里插入图片描述
内存的容量太小了,需要扩容,或者需要调整堆的空间。
错误的引用方式,发生了内存泄漏。没有及时的切断与 GC Roots 的关系。比如线程池里的线程,在复用的情况下忘记清理 ThreadLocal 的内容。
接口没有进行范围校验,外部传参超出范围。比如数据库查询时的每页条数等。
对堆外内存无限制的使用。这种情况一旦发生更加严重,会造成操作系统内存耗尽。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值