JVM内存结构详解(基于JDK8)

写在前面:博主是一位普普通通的19届二本大学生,平时最大的爱好就是听听歌,逛逛B站。博主很喜欢的一句话花开堪折直须折,莫待无花空折枝:博主的理解是头一次为人,就应该做自己想做的事,做自己不后悔的事,做自己以后不会留有遗憾的事,做自己觉得有意义的事,不浪费这大好的青春年华。博主写博客目的是记录所学到的知识并方便自己复习,在记录知识的同时获得部分浏览量,得到更多人的认可,满足小小的成就感,同时在写博客的途中结交更多志同道合的朋友,让自己在技术的路上并不孤单。

目录:
1.JVM内存结构总览及概述
2.方法区
3.Java堆
4.程序计数器
5.Java虚拟机栈
6.本地方法栈
7.运行时常量池

1.JVM内存结构总览及概述

在这里插入图片描述
我们可以看到JVM的内存分为三个部分

  • 类加载子系统
  • 运行时数据区
  • 执行引擎

运行时的数据区又分为五个部分

  • 方法区
  • Java栈(也称为Java虚拟机栈)
  • 本地方法栈
  • 程序计数器

其实我们平时说的JVM内存结构在某一角度讲是等同于JVM运行时数据区的,我们不必过于纠结两者的概念,特别注意其中Java 堆和方法区是 线程共享的。其他都是 线程私有的。

2.方法区

2.1方法区

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

2.2JDK8之前的方法区

就现在主流的HotSpot 虚拟机来说,在 JDK1.7的时候,方法区被称作为永久代, 从JDK1.8开始,方法区变成了元空间(Metaspace) (也就是废除了永久代的概念)。对于J9和JRockit这两类虚拟机来说是不存在永久代的概念的

永久代:
对于JDK8之前的永久代并不是,并不是数据进入方法区就 “永久存在” ,永久代区域的内存还是能回收的,回收的目标是针对常量池,和对类型的卸载

那么为啥要舍弃永久代使用原空间呢?
首先我们明确一点,为啥使用永久代呢,那是因为设计HotSpot的团队把收集器的分代设计扩展至方法区,或者说是使用永久代来实现方法区,因为使用永久代实现方法区可以使得HotSpot虚拟机的能够像管理堆一样管理这部内存,省去了专门为方法区编写内存管理代码的工作面试官问你你就说他们懒嘿嘿。但是这么设计比较容易出现内存泄漏(特别突出的例子就是在JDK7之前的HotSpot虚拟机中,String的intern()方法)抛出java.lang.OutOfMemoryError异常,因为永久代有内存上限(可以自己设置,但是不自己设置的情况下会有默认得到大小)。但是舍永久代后的元空间,元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而不会再出现像永久代的内存溢出错误了,也不会出现泄漏的数据移到交换区这样的事情。用户可以为元空间设置一个可用空间最大值,不设置默认根据类的元数据大小动态增加元空间的容量

Java堆

3.1Java堆的划分

Java 堆是所有线程共享的,它在虚拟机启动时就会被创建,而且是虚拟机管理的内存中最大的一块

Java 堆是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里。所以这里也就成为了垃圾回收器的主要活动营地了,于是它就有了一个别名叫做 GC 堆,并且单个 JVM 进程有且仅有一个 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

3.2-Xms和-Xmn参数

仔细观察上图可以看到-Xms和-Xmn的字样,是的这个正是控制堆的JVM的参数,实际上我们是可以通过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.lang.OutOfMemoryError异常

4.程序计数器

是一块较小的内存空间,可以看成是当前线程所执行的字节码的显示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码中指令。另外它是线程私有的,因为JVM的多线程是通过线程的轮流切换,分配处理器执行时间来实现的,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,因此为了线程切换后恢复到正确的执行位置,每一个线程都要有一个程序计数器,各个线程之间互不影响,独立存储

如果线程正在执行一个Java方法,那么记录的是正在执行的虚拟机字节码文件指令地址,如果执行的是一个本地方法(Native)那么这个计数器为空。而且此内存没有规定任何规定的异常(也就是说不会有溢出的异常警告)

5.Java虚拟机栈

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

局部变量表:

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

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

Java 虚拟机栈既允许被实现成固定的大小,也允许根据计算动态来扩展和收缩,如果采用固定大小的话,每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。在 Java 虚拟机栈中会发生两种异常,这个在虚拟机规范中有指出:
1.如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError异常;也就是栈溢出错误!方法递归调用产生StackOverflowError 异常这种结果。
2.如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的 Java 虚拟机栈,那么虚拟机将会抛出 OutOfMemoryError异常。也就是OOM内存溢出错误!(线程启动过多)
当然,可以通过参数 -Xss 去调整JVM栈的大小!

6.本地方法栈

1.和虚拟栈相似,只不过它服务于Native方法,线程私有。
2.与 Java 虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常。有的虚拟机(如HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

7.运行时常量池

我们补充一部分,运行时常量池是方法区的一部分

运行时常量池:用于存放编译期生成的各种字面量与符号的引用,这部分内容是将在类加载后存放到方法区的运行时常量池中的,特别注意,Java不要求常量一定要在编译期产生,运行时也可以吧常量放在池中

参考博客:
JVM 内存结构基于JDK1.8【JVM篇三】

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45737068/article/details/107129074
今日推荐