一、JVM内存概述
面试题
- 说一说jvm内存结构是什么样子,每个区域放什么,各有什么特点?
- JVM内存结构及各个结构内容
- jvm内存模型有哪些?
- jvm内存划分为哪些区域
- jvm内存模型
- jvm内存分为哪几个区,每个区的作用
- 详解jvm内存模型
- jvm由哪些组成,堆栈各放了什么东西
- jvm的内存模型,线程独有的放在哪里?哪些是线程共享的?哪些是线程独占的?
- 讲一下为什么jvm要分为堆、方法区等,原理是什么?
二、程序计数器(了解)
作用:记录下一条指令的地址,执行引擎的字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
特点:他是jvm中内存最小的区域,也是运行速度最快的区域,不会随着程序的运行需要更大的存储空间,不会报错out of memeory
三、栈
无论是操作数栈还是局部变量表,它们的长度都是在编译期可以确定的
面试题
- 堆和栈的区别,谁的性能更高?
- 为什么要把堆和栈分出来?栈不是也可以存放数据吗?
要点
- GC;OOM
- 执行效率
- 内存大小
- 栈管运行、堆管存储
答:栈是一种先进先出的数据结构,非常适合记录方法的嵌套调用,并且栈的内存较小,运行速度块,所以栈一般负责记录线程的运行,而堆内存较大,适合存储引用对象,如果用栈来存储引用对象,那么栈很容易报OutOfMemoryError,并且运行速度也会下降
栈可能抛出的异常
StackOverflowError和OutOfMemory,StackOverflowError通常出现在方法递归比较深超出栈的容量,而OutOfMemoryError出现在高并发的时候,jvm没有多余的内存去创建栈
栈帧中的组件
局部变量表、操作数栈、动态链接、方法返回信息
局部变量表
局部变量表是一个数组,记录该方法中的变量。
- double和Long会占据局部变量表两个slot
- 局部变量表的大小在编译期确定
- 栈帧中的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的
- 方法嵌套大小由栈大小决定,对一个函数而言,局部变量越多导致局部变量表膨胀,它的栈帧就越大,导致嵌套调用次数越少
- 局部变量表只在当前方法有效,方法执行结束局部变量表也就会销毁
操作数栈
栈顶缓存技术
将操作数栈的栈顶元素全部缓存到cpu的寄存器中,提高执行效率
面试题
- 栈溢出的情况
- 调整栈大小就能保证不溢出吗
- 分配的栈内存越大越好吗
- 垃圾回收是否会涉及到虚拟机栈
- 方法中定义的局部变量是否线程安全
提示:GC只会存在堆和方法区中
四、堆
核心概述
- 一个jvm实例只存在一个堆内存,它是java内存管理的核心区域
- 堆在jvm启动的时候被创建,空间大小也是在启动时就确定了,是jvm管理的最大一块内存空间
- 堆大小是可以调节的
- 它是一个物理上不连续但逻辑上联系的空间
- 是垃圾回收的重点区域
面试题
- Java中所有对象都在堆上吗?
答:几乎是,jvm规范中写到 所有的class实例和数组都应该在运行时分配在堆上 - 堆的结构是什么样子?
- 堆为什么要分为新生代、老年代、持久代、新生代中为什么要分为Eden和Survivor?
- 堆里面的分区:Eden、survival、老年代各自的特点
- 堆的结构?为什么有两个survival区
- Eden和survivor区的比例
提示
分代的原因:优化GC,如果没有分代,那么每次垃圾回收都需要从头到尾扫描依次,非常耗费性能。而大多数对象都是朝生夕死的,如果把新创建的对象放到某个地方,当GC时首先对它进行回收,这样做能节省很多时间并且腾出的空间也不比之前说的差多少
- 堆大小通过什么参数设置? -Xms 起始堆内存 -Xmx 最大堆内存
- 初始堆大小和最大堆大小一样,这样设置有什么好处?
答:目的是为了能够让jvm垃圾回收清理完堆区后不需要重新分隔计算堆区大小,从而提高性能 - jvm最大堆大小有没有限制?
答:jvm堆最大限制不超过物理内存1/4,默认为物理内存1/64
TLAB
JVM对Eden进行划分,为每个线程分配一个私有缓存区域。堆区是线程共享区域,任何线程都可以访问到堆区的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
参数设置
- 配置新生代和老年代比例:-XX:NewRatio=2(默认),代表老年代:新生代 = 2:1
- 新生代最大内存大小:-Xmn
- eden和survivor比例:-XX:SurvivorRatio=8(默认)
对象分配过程
结论
- 针对幸存区s0,s1,复制之后会交换,谁空谁是s0
- 垃圾回收:
- 频繁在新生代收集
- 很少在老年代收集
- 几乎不在永久代\元空间收集
动态年龄判断
**如果Survivor区相同年龄的所有对象大小的总和大于Survivor区的一半,则大于或等于该年龄的所有对象都可以进入到老年区**
空间分配担保
在发生young GC之前,jvm会检查老年代最大可用连续空间是否大于新生代所有对象的总空间
- 若大于,则此时young GC是安全的
- 否则,继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象平均大小,如果大于则尝试进行依次Young GC (这次Young GC仍然是有风险的),否则进行依次Full GC
MinorGC、Major GC和Full GC的区别
jvm将GC按照回收区域分成两大类型:
- 部分收集(Partial GC)
- 整堆收集(Full GC)
部分收集只针对某个分区:
- 新生代收集(young GC 或minor GC)
- 老年代收集(Major GC):目前只有CMS会单独收集老年代
- 混合收集(Mixed GC):收集整个新生代以及部分老年代,目前只有G1会这样
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
Full GC触发机制
- 老年代空间不足
- 方法区空间不足
- minor GC后进入到老年代的平均大小小于老年代的可用内存
OOM如何解决
- 一般先通过内存映像分析工具堆dump出来的堆转储快照分析,重点是确认内存中的对象是否是必要的,也就是先分清到底是出现了内存泄漏还是内存溢出
- 如果是内存泄露,进一步通过工具查看泄露对象到GC Roots的引用链,就能找到泄露对象是通过怎样的路径与GC Roots相关联,通过GC Roots和引用链信息就可以比较准确的定位出泄露代码的位置
- 如果不是内存泄漏,那就应该检查虚拟机参数与物理内存对比看是否还能再调大,聪慧代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗
方法区
方法区存放类的信息,比如方法元、常量池、类元信息等信息
jdk7及之前中用永久代实现方法区,jdk8以后用元空间实现方法区
原因:永久代属于堆的一部分,需要设置合适的永久代大小否则容易OOM,而元空间使用的是直接内存则不会出现该问题
面试题
- JDK7到8中方法区的变化
- 为什么在jdk8中将stringtable放到了堆中
答:因为字符串经常被创建,如果不能及时回收,很容易占满方法区。