Java8常量为什么放在堆内存,大厂面试必考-JVM 内存管理 Java8
大厂面试必考-JVM 内存管理 Java8
大厂面试必考-JVM 内存管理 Java8
文章目录大厂面试必考-JVM 内存管理 Java8前言
1.为什么要进行内存区域划分?
2.JVM 内存布局
3.虚拟机栈
4.程序计数器
5.堆
6.元空间
7.其他面试题7.1 常量池去哪里了?
7.2 堆、非堆、本地内存,有什么关系?
8.总结8.1 JVM 如何进行内存区域的划分?
8.2 JVM 如何高效进行内存管理?
8.3 Java8 为什么增加了元空间,它又涉及什么问题?
9.参考
前言
本章我们将讨论 JVM 的内存划分以及栈上的执行过程。这块内容在面试中主要涉及以下三个问题:
JVM 如何进行内存区域的划分?
JVM 如何高效进行内存管理?
Java8 为什么增加了元空间,它又涉及什么问题?
1.为什么要进行内存区域划分?
先来讨论为什么要进行内存区域划分?因为 Java 和其他语言相比(比如 C++),最引以为豪的特性就是自动内存管理机制。然而这种自动的内存申请和释放方式,自然也有它的代价。为了管理这些快速的内存申请释放操作,就必须引入一个池子来延迟这些内存区域的回收操作。我们常说的内存回收,就是针对这个池子的操作。我们这个池子叫作堆。
简而言之,基于两点原因要进行内存区域划分
JVM 的自动内存管理机制。
内存区域划分可以更加高效的管理内存申请释放操作。
接下来我们继续看 内存区域 究竟如何划分?
2.JVM 内存布局
程序想要运行,就需要数据。有了数据,就需要在内存上存储。Java 程序的数据结构是非常丰富的。其中的内容,举一些例子:
静态成员变量
动态成员变量
区域变量
短小紧凑的对象声明
庞大复杂的内存申请
这么多不同的数据结构,到底是在什么地方存储的,他们之前又是如何进行交互的?
我们先看一下 JVM 的内存布局。随着 Java 的发展,内存布局一直在调整之中。比如,Java 8 及之后的版本,彻底移除了持久代,而使用 Metaspace 来进行替代。这也表示着 -XX:PermSize 和 -XX:MaxPermSize 等参数调优,已经没有了意义。但大体上,比较重要的内存区域是固定的。
文章图片
JVM 内存区域划分如图所示,从图中我们可以看出:
JVM 堆中的数据是共享的,占用内存最大的一块区域。
可以执行字节码的模块叫做执行引擎。
执行引擎在线程切换时怎么恢复?依靠的就是程序计数器。
JVM 的内存划分与多线程是息息相关的。像我们程序中运行时用到的栈,以及本地方法栈,它们的维度都是线程。
本地内存包含元数据区和一些直接内存。
3.虚拟机栈
栈的数据结构可以参考博主之前的文章 《数据结构与算法|第五章:栈》,文章中的第四小节也会简单介绍虚拟机栈和操作数栈的数据结构。
Java 虚拟机栈是基于线程的,哪怕只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。每个栈帧,都包含四个区域:
局部变量表
操作数栈
动态连接
返回地址
我们的应用程序,就是在不断操作这些内存空间中完成的。
文章图片
本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。甚至可以认为虚拟机栈和本地方法栈是同一个区域,这并不影响我们对 JVM 的了解。
这里有一个比较特殊的数据类型叫作 returnAdress。因为这种类型只存在于字节码层面,所以我们平常打交道的比较少。对于 JVM 来说,程序就是存储在方法区的字节码指令,而 returnAddress 类型的值就是指向特定指令内存地址的指针。
文章图片
其实这里的数据结构可以看成一个“栈中栈”,有一个两层的栈
第一层是栈帧,对应着方法
第二层是方法的执行,对应着操作数
所有的字节码指令,其实都会抽象成入栈/出栈的操作。执行引擎只需要傻瓜式的按顺序执行,就可以保证它的正确性。
4.程序计数器
那么你设想一下,如果我们的程序在线程之间进行切换,凭什么能够知道这个线程已经执行到什么地方呢?
既然是线程,就代表它在获取 CPU 时间片上,是不可预知的,需要有一个地方,对线程正在运行的点位进行缓冲记录,以便在获取 CPU 时间片时能够快速恢复。
就好比你停下手中的工作,倒了杯茶,然后如何继续之前的工作?
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。这里面存的,就是当前线程执行的进度。下面这张图,能够加深大家对这个过程的理解。
文章图片
可以看到,程序计数器也是因为线程而产生的,与虚拟机栈配合完成计算操作。程序计数器还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处理等。
我们可以看一下程序计数器里面的具体内容。下面这张图,就是使用 javap 命令输出的字节码。大家可以看到在每个 opcode 前面,都有一个序号。就是图中红框中的偏移地址,你可以认为它们是程序计数器的内容。
文章图片
5.堆
文章图片
堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。
堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。
随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。
那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。
Java 的对象可以分为基本数据类型和普通对象。
对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。我们上面提到,每个线程拥有一个虚拟机栈。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。
注意,像 int[] 数组这样的内容,是在堆上分配的。数组并不是基本数据类型。
以上就是 JVM 的基本的内存分配策略。
6.元空间
关于元空间,我们还是以一个非常高频的面试题开始:“Java 8 为什么增加了元空间,它有什么问题?”
回想一下类与对象的区别。对象是一个活生生的个体,可以参与到程序的运行中;类更像是一个模版,定义了一系列属性和操作。可以设想一下,我们前面生成的 A.class,是放在 JVM 的哪个区域的?
想要问答这个问题,就不得不提下 Java 的历史。在 Java 8 之前,这些类的信息是放在一个叫 Perm 区的内存里面的。更早版本,甚至 String.intern 相关的运行时常量池也放在这里。这个区域有大小限制,很容易造成 JVM 内存溢出,从而造成 JVM 崩溃。
Perm 区在 Java 8 中已经被彻底废除,取而代之的是 Metaspace。原来的 Perm 区是在堆上的,现在的元空间是在非堆上的,这是背景。关于它们的对比,可以看下这张图。
文章图片
元空间的好处也是它的坏处。使用非堆可以使用操作系统的内存,JVM 不会再出现方法区的内存溢出;但是,无限制的使用会造成操作系统的死亡。所以,一般也会使用参数 -XX:MaxMetaspaceSize 来控制大小。
方法区,作为一个概念,依然存在。它的物理存储的容器,就是 Metaspace。你只需要了解到,这个区域存储的内容,包括:类的信息、常量池、方法数据、方法代码就可以了。
7.其他面试题
7.1 常量池去哪里了?
JVM中存在多个常量池
字符串常量池,已经移动到堆上(jdk8之前是perm区),也就是执行 intern 方法后存的地方。
类文件常量池,constant_pool,是每个类每个接口所拥有的,字节码中“#n”的那些都是。这部分数据在方法区,也就是元数据区。
文章图片
运行时常量池是在类加载后的一个内存区域,它们都在元空间。
7.2 堆、非堆、本地内存,有什么关系?
他们之前的内存划分如图:
文章图片
JVM 在运行时,会从操作系统申请大块的堆内内存,进行数据的存储。但是,堆外内存也就是申请后操作系统剩余的内存,也会有部分受到 JVM 的控制。比较典型的就是一些 native 关键词修饰的方法,以及对内存的申请和处理。
在 Linux 机器上,使用 top 或者 ps 命令,在大多数情况下,能够看到 RSS 段(实际的内存占用),是大于给 JVM 分配的堆内存的。
举个例子(Java 8):
假设操作系统有8G。-Xmx分配了4G(堆内内存),Metaspace使用了256M(堆外内存),剩下的 8G-4G-256M ,就是操作系统剩下的本地内存。
具体本地内存如何变成堆外内存,要看情况。 比如:
netty 的 direct buffer 使用了额外的 120MB 内存,那么现在 JVM 占用的堆外内存就有 256M+120M
使用了 jni 或者 jna,直接申请了内存 2GB,那么现在 JVM 占用的堆外内存就有 256M+120M+2GB
网络 socket 连接等,占用了操作系统的 50MB 内存 这个时候,留给操作系统的就只剩下了:8GB-4GB-256M-120M-2GB-50M。
具体“堆和堆外”一共用了多少,可以top命令,看RSS段。·
8.总结
我们来总结下开篇的三个问题
8.1 JVM 如何进行内存区域的划分?
这个问题可以从两个角度来回答
第一 JVM 规范角度
JVM 规范《The Java Virtual Machine Specification, Java SE 8 Edition》2.5 Run-Time Data Areas
中描述的运行时数据区分为 6 个区域
程序计数器(The pc Register)
虚拟机栈(Java Virtual Machine Stacks)
堆(Heap)
方法区(Method Area)
运行时常量池(Run-Time Constant Pool)
本地方法栈(Native Method Stacks)
第二具体虚拟机实现角度
Java 8 内存区域划分大致分为 6 个区域:
线程私有的三个区域,分别是虚拟机栈、本地方法栈、程序计数器
本地内存(或者说非堆)包含元数据区和直接内存
元数据区包含方法区和运行时常量池
最后是堆(Heap)区,它是 JVM 上最大的内存区域
8.2 JVM 如何高效进行内存管理?
内存区域的合理划分
垃圾回收
8.3 Java8 为什么增加了元空间,它又涉及什么问题?
增加元空间的好处是。使用非堆可以使用操作系统的内存,JVM 不会再出现方法区的内存溢出。
但是同时带来的坏处是,无限制的使用会造成操作系统的死亡。所以,一般也会使用参数 -XX:MaxMetaspaceSize 来控制大小。
9.参考
《深入浅出 Java 虚拟机》- 李国
【Java8常量为什么放在堆内存,大厂面试必考-JVM 内存管理 Java8】大厂面试必考-JVM 内存管理 Java8相关教程
推荐阅读
- 编程为什么有趣(浅谈编程的快乐。)
- 为什么在数据驱动的路上,AB 实验值得信赖()
- 浅谈(为什么vue和react都选择了Hooks?)
- Java8新特性Optional类在处理空值判断场景的应用 回避空指针异常 编写健壮的应用程序
- 为什么短信链接总是被拦截(应该如何避免拦截?)
- #yyds干货盘点# 京东二面,Redis为什么这么快()
- Python从数据集中移除常量特征介绍
- #yyds干货盘点#公司规定所有接口都用 POST请求,这是为什么()
- docker|docker入门——简介
- CPU中的MESI缓存最终一致性---CPU为什么需要缓存