Java虚拟机的内存结构

1.运行时内存区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。

JDK 1.8 之前:

JDK 1.8 :
在这里插入图片描述
在这里插入图片描述
线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

1.1.程序计数器

当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

由于Java多线程是通过轮流切换线程来实现的,一个时刻、一个处理器(多核处理器的一个内核)只能执行一条线程中的指令,为了线程切换后能恢复到正确的位置,每个线程应该有独立的程序计数器,所以程序计数器是线程私有的数据区

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

1.2.Java虚拟机栈

Java虚拟机栈与线程同时创建,生命周期与线程相同。

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)

局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。

  • StackOverflowError异常:线程请求的栈深度大于虚拟机允许的深度。例如:死循环调用一个方法,不停有栈帧入栈,直至栈满。

  • OutOfMemoryError异常:如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,会抛出OutOfMemoryError异常。

Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

扩展:那么方法/函数如何调用?

Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

Java 方法有两种返回方式:

  1. return 语句。
  2. 抛出异常。

不管哪种返回方式都会导致栈帧被弹出。

1.3.本地方法栈

与虚拟机栈的作用非常相似,虚拟机栈执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,有的虚拟机没有这个东西。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。

1.4.堆

Java堆在虚拟机启动时创建,此内存区域唯一的目的是存放对象实例,几乎所有的对象实例都在这里分配内存。之所以说几乎是因为有特殊情况,有些时候小对象会直接在栈上进行分配,这种现象我们称之为“栈上分配”。

Java堆是垃圾收集器管理的主要区域,很多时候也被称作GC堆。

(1). 从内存回收的角度看,现在收集器都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代。年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在这里插入图片描述
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永生代(Permanent Generation)

在这里插入图片描述

JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。

上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间一层属于老年代。

当有对象需要分配时,一个对象永远优先被分配在年轻代的 Eden 区,等到 Eden 区域内存不够时,Java 虚拟机会启动垃圾回收。此时 Eden 区中没有被引用的对象的内存就会被回收,而一些存活时间较长的对象则会进入到老年代。在 JVM 中有一个名为 -XX:MaxTenuringThreshold 的参数专门用来设置晋升到老年代所需要经历的 GC 次数,即在年轻代的对象经过了指定次数的 GC 后,将在下次 GC 时进入老年代。

这里让我们思考一个问题:为什么 Java 堆要进行这样一个区域划分呢?

根据我们的经验,虚拟机中的对象必然有存活时间长的对象,也有存活时间短的对象,这是一个普遍存在的正态分布规律。如果我们将其混在一起,那么因为存活时间短的对象有很多,那么势必导致较为频繁的垃圾回收。而垃圾回收时不得不对所有内存都进行扫描,但其实有一部分对象,它们存活时间很长,对他们进行扫描完全是浪费时间。因此为了提高垃圾回收效率,分区就理所当然了。

(2). 从内存分配的角度看,Java堆是线程共享的,它可以划分出多个线程私有的分配缓冲区,不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都是对象实例。

如果堆中没有内存完成实例分配,也无法再扩展时,抛出OutOfMemoryError异常。并且出现这种错误之后的表现形式还会有几种,比如:

  1. OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space 错误。(和本机物理内存无关,和你配置的内存大小有关!)

1.5.方法区

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

习惯HotSpot虚拟机的开发者很多更愿意把方法区叫做“永久代”,本质上两者并不一样:

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法

JDK1.7中已经将原本放在永久代的字符串常量池移出。取而代之是元空间,元空间使用的是直接内存。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

  1. 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

  1. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

  2. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

1.7的方法区和永久代,并不是等号。方法区是堆的一个逻辑部分,只是重新起来一个名字叫做非堆。永久代只是方法区的一个实现,为的是方法区也可以用堆内存的GC垃圾回收器,而不用重新针对方法区做GC操作,都用堆内存的GC就行了,所以称永久代是方法区的一个存储实现。而且1.8版本的元数据区域不是在堆内存中,它使用的是本地内存,所以1.8元空间不存在OOM内存溢出的情况,1.7版本永久代是存在内存溢出的。你说的的常量池在堆空间,只是string的实例化对象实际是放在堆空间的,其实常量池在1.8中就是放在了元空间

1.6.运行时常量池

jdk1.8中永久代被元数据替换,类的属性,常量,方法等都存储在元数据区,但是字符串常量池,静态变量实际上是保存在堆中的。

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
在这里插入图片描述

宋红康jvm最新视频(2020年)提到了网上各种说法很乱的问题, 然后他根据官方文档和《深入理解java虚拟机》第三版(最新一版),总结了如下:
jdk1.6以前:有永久代(也可以说是方法区),静态变量存放在永久代上。
jdk1.7:有永久代,但已经逐步“去永久代化”,字符串常量池,静态变量移除,保存在堆中。
jdk1.8及以后:无永久代,类型信息,字段,方法,常量,保存在本地内存的元空间,但字符串常量池,静态变量仍在堆。

参考:https://blog.csdn.net/alex6586/article/details/107757796

1.7.直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值