49.【面试宝典】面试宝典-JVM内存模型

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 【面试宝典】面试宝典-JVM内存模型

前文如上:

39.【面试宝典】面试宝典-redis过期k值回收策略,缓存淘汰策略

40.【面试宝典】面试宝典-redis持久化

41.【面试宝典】面试宝典-redis常用数据类型概述

42.【面试宝典】面试宝典-redis缓存穿透,击穿,雪崩

43.【面试宝典】面试宝典-redis缓存穿透之布隆过滤器

44.【面试宝典】面试宝典-redis分布式锁

45.【面试宝典】面试宝典-另外两种分布式锁

46.【面试宝典】面试宝典-虚拟机类加裁机制

47.【面试宝典】面试宝典-类加载器及双亲委派

48.【面试宝典】面试宝典-如何打破双亲委派机制

合集参考:面试宝典


作为一名JAVA骨灰级菜鸟开发(工作七年多了),Java虚拟机是必备的技能,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。


内存模型


首先就是JVM的内存模型


网络异常,图片无法展示
|


从图中,可以看到整个Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。 而这些区域分为线程共有和线程私有两部分。


网络异常,图片无法展示
|


线程私有的


  • 程序计数器
  • 一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的 程序计数器,这类内存也称为“线程私有”的内存。正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
  • 虚拟机栈
    是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( DispatchException)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异 常)都算作方法结束。
  • 本地方法栈
    本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为 Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个 C 栈,但HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。


线程共享的


是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为:新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。


新生代: 是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。分别占比8:1:1


Eden 区: Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行 一次垃圾回收。ServivorFrom上一次 GC 的幸存者,作为这一次 GC 的被扫描者。ServivorTo 保留了一次 MinorGC 过程中的幸存者。


MinorGC 采用复制算法。MinorGC 的过程(复制->清空->互换),新生代的minorGC简单介绍如下:

(1)eden、servicorFrom 复制到 ServicorTo,首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准(默认年龄加到了15),则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不 够位置了就放到老年区);

(2)然后,清空 Eden 和 ServicorFrom 中的对象;

(3)最后,ServicorTo 和 ServicorFrom 角色互换,原 ServicorTo 成为下一次GC 时的 ServicorFrom 区。


老年代: 主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。


MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出OOM(Out of Memory)异常。


扩展:对象何时进入老年代


(1)躲过15次GC之后进入老年代:系统刚启动时,创建的各种各样的对象,都是分配在年轻代里。随着慢慢系统跑着跑着,年轻代满了,就会出发Minor GC ,有的对象可能存活时间会久一些,无论年轻代中怎么垃圾回收,类似这种对象都不会被回收掉。而此对象每次在年轻代里躲过一次Minor GC被转移到一块Survivor区域中,他的年龄就会增加一岁默认的设置下,当对象的年龄达到15岁时,也就是躲过15次GC的时候,他就会转移到老年代里去。具体是多少岁进入老年代,可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁。


(2)动态对象年龄判断:对于对象年龄,有另外一个规则可以让对象进入老年代,不用等到15次GC过后才可以。规则就是,假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。

网络异常,图片无法展示
|


假设这个图中的Survivor2区有两个对象,这俩对象的年龄一样,都是2岁。然后俩对象加起来内存超过了50MB,这个时候,Survivor2区里大于等于2岁的对象,都要进入老年代里去。

这就是动态年龄判断的规则,这条规则也会让一些年轻代的对象进入老年代另外实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区的50%,此时就会把年龄n以上的对象都放入老年代。


(3)大对象直接进入老年代:有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把它的值设置为字节数,比如“1048576”字节,就是1MB。意思就是如果你要创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放到老年代里去,压根不会经过年轻代。之所以这么做,是因为要避免年轻代里出现那种大对象,然后屡次躲过GC,还得把他在两个Survivor区域里来回复制多次之后才能进入老年代。(减少开销)


(4)Minor GC后的对象太多,无法放入Survivor区:这个比较好理解,因为新生代分为 Eden 区、ServivorFrom、ServivorTo 三个区,分别占比8:1:1,复制清楚粘贴到ServivorTo 空间不足呀


(5)老年代空间分配担保规则:如果年轻代里大量对象存活,确实自己的Survivor区放不下了,必须转移到老年代去,但是如果老年代里空间也不够放这些对象,改怎么办呢?(说白了就是老年代也放不下年轻代存活下来的对象了,就会先去判断自己要不要进行Full GC,然后再minor GC把年轻代存活的的对象挪过来)

首先,在执行任何一次Minor GC之前,JVM都会先检查一些老年代可用的内存空间,是否大于年轻代所有对象的总大小.

网络异常,图片无法展示
|

在最极端的情况下,可能年轻代Minor GC之后,所有对象都存活下来了,在执行Minor GC之前,发现老年代的可用内存已经小于了年轻代的全部对象大小了. 那么老年代内存空间就不够了?

首先如Minor GC之前,发现老年代的可用内存已经小于了年轻代的全部对象大小,就会看一个“-XX:-HandlePromotionFailure”的参数是否设置了,如果设置了这个参数接下来会看看老年代的内存大小,是否大于之前每一次Minor GC后进去老年代对象的平均大小, 但是如果上面步骤判断失败了,或者是“-XX:-HandlePromotionFailure”参数没设置,此时就会直接触发一次“Full GC”,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC。

如果上面两个步骤判断成功,那么就可以尝试Minor GC,此时进行Minor GC有几种可能:

①Minor GC过后,剩余的存活对象的大小,小于Survivor区的大小,那么此时存活对象进入Survivor区即可

②Minor GC过后,剩余的存活对象的大小,大于Survivor区的大小,但是小于老年代可用内存大小,就直接进入老年代即可\

③Minor GC过后,剩余的存活对象的大小,大于Survivor区的大小,同时大于老年代可用内存大小,此时就会发生“Handle Promotion Failure”的情况,这个时候就会出发一次“Full GC”。

Full GC就是对老年代进行垃圾回收,同时也一般会对年轻代进行垃圾回收。如果Full GC之后,老年代还是没有足够空间存放Minor GC过后的剩余存活对象,此时就会导致所谓的“OOM”内存溢出了。


(6).老年代垃圾回收算法

老年代触发垃圾回收的机制,一般就是两个;

①在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需要提前触发Full GC再然后再带着进行Minor GC;

②要不然是在Minor GC之后,发现剩余对象太多放入老年代都放不下了。

老年代采取的垃圾回收算法是标记整理算法。

老年代的垃圾回收算法的速度至少比年轻代的垃圾回收算法的速度慢10倍。

如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。

所以,所谓的JVM优化,就是尽可能的让对象都在年轻代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免年轻代频繁的进行垃圾回收。


参考:blog.csdn.net/wangshiwen0…


方法区(Heap-线程共享)-运行时数据区


  • 即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、 态变量、即时编译器编译后的代码等数据.
  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java 虚拟机对 Class 文件的每一部分(自然也包括常量 池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会 被虚拟机可、装载和执行。GC 不会在主程序运行期对永久区域进行清理。所以这 也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。(1.6和1.8异常不一样)
    在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。 元空间 的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,不再由 MaxPermSize控制,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。 jdk1.8中:类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,所以注意string 字符串1.6("计算机","java" false,false)和1.8("计算机","java" true,false) ==的区别。


直接内存

      直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。直接内存是Java堆外的、直接向系统申请的内存区间。大小和系统硬件内存有关,一般NIO用的多。


公众号,感谢关注

网络异常,图片无法展示
|



相关文章
|
24天前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
22天前
|
存储 算法 Oracle
不好意思!耽误你的十分钟,JVM内存布局还给你
先赞后看,南哥助你Java进阶一大半在2006年加州旧金山的JavaOne大会上,一个由顶级Java开发者组成的周年性研讨会,公司突然宣布将开放Java的源代码。于是,下一年顶级项目OpenJDK诞生。Java生态发展被打开了新的大门,Java 7的G1垃圾回收器、Java 8的Lambda表达式和流API…大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
不好意思!耽误你的十分钟,JVM内存布局还给你
|
30天前
|
存储 算法 Java
JVM自动内存管理之垃圾收集算法
文章概述了JVM内存管理和垃圾收集的基本概念,提供一个关于JVM内存管理和垃圾收集的基础理解框架。
JVM自动内存管理之垃圾收集算法
|
30天前
|
存储 Java 程序员
JVM自动内存管理之运行时内存区
这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
JVM自动内存管理之运行时内存区
|
1月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
27天前
|
缓存 算法 Java
这些年背过的面试题——JVM篇
本文是技术人面试系列JVM篇,面试中关于JVM都需要了解哪些基础?一文带你详细了解,欢迎收藏!
|
29天前
|
存储 安全 Java
JVM内存结构
这篇文章详细介绍了Java虚拟机(JVM)的内存结构,包括类的加载过程、类加载器的双亲委派机制、沙箱安全机制、程序计数器、Java栈、Java堆、本地方法和本地方法栈等关键组件及其作用。
JVM内存结构
|
2月前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
|
2月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
|
29天前
|
算法 Java
JVM自动内存管理之垃圾收集器
这篇文章是关于Java虚拟机(JVM)自动内存管理中的垃圾收集器的详细介绍。