02Java虚拟机内存模型概况——Java内存区域与内存溢出

02Java虚拟机内存模型概况——Java内存区域与内存溢出,第1张

02Java虚拟机内存模型概况——Java内存区域与内存溢出 [02]Java虚拟机内存模型概况——Java内存区域与内存溢出

aothor:陈镇坤27

创建时间:2021年12月13日23:07:32

文章目录
  • [02]Java虚拟机内存模型概况——Java内存区域与内存溢出
    • 一、Java虚拟机内存模型概况
      • 问:JVM的运行时数据区域有哪些哪些部分?
      • 问:什么是虚拟机概念模型?
      • 问:简单介绍下程序计数器。
      • 问:为什么程序计数器是私有的?
      • 问:什么是虚拟机栈?
      • 问:比较C语言中的内存布局,Java与之对标的”栈内存“是什么?
      • 问:Java虚拟机栈栈帧中的局部变量表指什么?
      • 问:什么是本地方法栈?
      • 问:什么是Java堆?
      • 问:老年代、年轻代这些,是Java堆的固有内存布局吗?为什么?
      • 问:java堆中是否还有细分的结构?
      • 问:java堆的是物理上连续的内存吗?
      • 问:java堆相关参数有哪些。
      • 问:什么是方法区?
      • 问:方法区是永久代吗?不是的话,产生了什么问题?
      • 问:什么是运行时常量池?
      • 问:Class文件包含了什么内容?
      • 问:什么是直接内存?
    • 二、HotSpot虚拟机对象创建始末
      • 问:对象是如何创建的?
      • 问:什么是分配内存时的“指针碰撞”和“空闲列表”?
      • 问:关于对象创建,在并发情况下可能产生什么问题?解决方法是什么?
      • 问:对象的内存是如何布局的?
      • 问:对象的对象头主要包含什么内容?
      • 问:对象的实例数据部分存储什么内容?
      • 问:对象中的对齐填充是什么?
      • 问:如何访问对象?
    • 三、OutOfMemoryError
      • 问:内存溢出和内存泄露的区别是什么?
      • 问:怎么设置Java堆大小?
      • 问:怎么分析Java堆的内存溢出异常?
      • 问:怎么设置虚拟机栈和本地方法栈大小?
      • 问:虚拟机栈可能抛出什么异常?
      • 问:虚拟机栈的最小值和最大值为多少,怎么理解?
      • 问:Hopspot虚拟机下,线程在什么情况下能出现OutOfMemoryError?
      • 问:32位的系统,在不能进行升级和提升机器性能的情况下,怎么降低频繁创建线程时出现OutOfMemoryError的概率?
      • 问:String::intern()方法的作用是什么?在永久代和元数据空间之中,有什么区别?
      • 问:JDK6及以前的永久代设置参数是多少?
      • 问:在JDK6以JDK8下,下列代码的执行结果分别是什么?为什么?
      • 问:在实际生产中,什么因素影响方法区?
      • 问:在JDK8及以后,设置元空间大小的参数是什么?
      • 问:什么是直接内存溢出?

——————————————————————————————

一、Java虚拟机内存模型概况 问:JVM的运行时数据区域有哪些哪些部分?

答:根据《Java虚拟机规范》规定,主要包含运行时数据区(方法区、堆、虚拟机栈、本地方法栈、程序计数器)、执行引擎(共有)、本地库接口(共有)。

问:什么是虚拟机概念模型?

答:虚拟机概念模型是虚拟机的“统一外观”,不规划具体实现。

PS:类似于数据库设计过程(中级软件分析师)的概念结构设计。

问:简单介绍下程序计数器。

答:每个线程都会分配到一个私有的程序计数器(行号指示器),指示当前线程所执行字节码。字节码解释器工作时,通过改变程序计数器的值,来选取字节码指令。当线程执行的是Java方法时,程序计数器存储虚拟机字节码指令地址。如果执行的是Native方法,则值为Undefined。

PS:该区域是唯一一个在《Java虚拟机规范》中不要求抛出OutOfMemory的内存区域。

问:为什么程序计数器是私有的?

答:多线程系统中,有限的CPU资源被多线程切换使用,为了确保线程切换后,能够从上一个停止的位置恢复执行,因此需要确保程序计数器是线程私有的。

问:什么是虚拟机栈?

答:是描述Java执行的线程内存的模型,线程每调用一个Java方法,都会创建一个栈帧,(像d夹一样)压入虚拟机栈。栈帧存储局部变量表、 *** 作数栈、动态连接、方法出口等信息。

问:比较C语言中的内存布局,Java与之对标的”栈内存“是什么?

答:C语言主要分堆内存栈内存,在Java中讲栈内存时,更多时候是在指Java虚拟机栈的栈帧中的局部变量表。

问:Java虚拟机栈栈帧中的局部变量表指什么?

答:其中存放Java方法编译期确定的各种数据类型

基本数据类型数据,包括:boolean、byte、char、short、int、float、double、long

对象引用(reference):指向对象起始地址的引用指针/指向代表对象的句柄/其他与此对象相关的位置

returnAddress:字节码指令地址

编译期还同时确定了栈帧局部变量表需要的空间,整个局部变量表在运行时不会发生改变(ClassicVM不清楚)。以上的数据存储中,long和double存储占用两个局部变量槽(Slot),其他的类型每个都只占用一个Slot,但具体每个Slot占用多少个bit,由虚拟机自身去实现(至少要大于或等于32bit)。

PS:HotSpot虚拟机不支持栈动态扩展,当在一个栈中创建超出阈值的栈时,在申请过程中,虚拟机会计算并抛出OOM(这是计算之后抛出的,而没有去真实地申请)。

PS:虚拟机栈帧结构的其他中文描述(不要和局部变量表的结构搞混):

问:什么是本地方法栈?

答:比较虚拟机栈为虚拟机使用Java方法服务,本地方法栈为虚拟机使用Native方法服务。《Java虚拟机规范》并没有要求本地方法栈的具体实现(语言、结构等),HotSpot干脆将虚拟机栈和本地方法栈合二为一。

问:什么是Java堆?

答:“所有的对象实例以及数组都应当在堆上分配”

因此Java堆成为了垃圾收集器主要管理的区域,由此也衍生出另一个称呼——GC堆。

问:老年代、年轻代这些,是Java堆的固有内存布局吗?为什么?

答:不是。它们是基于经典分代收集理论而设计的垃圾收集器的内部逻辑设计,是一种共同设计特性。既不是某个特定的虚拟机的内部固有内存布局,也不是《Java虚拟机规范》设计要求。

在过去(G1收集器(09年JDK7)出现以前),垃圾收集基本都是基于分代理念设计的,所以在描述Java堆内存时,这种说法不会产生太大歧义,但已然不适用于现代。

问:java堆中是否还有细分的结构?

答:在共享的堆中可划分出线程私有的分配缓存区(Thread local Allocation Buffer,TLAB),目的是为了加快内存分配和回收。

问:java堆的是物理上连续的内存吗?

答:《Java虚拟机规范》仅要求其逻辑上连续。

问:java堆相关参数有哪些。

答:

-Xmx   -Xms
问:什么是方法区?

答:《Java虚拟机规范》将方法区描述成堆的一个逻辑部分,但为区分,方法区也称“非堆”。

方法区由线程共享,存储被加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

问:方法区是永久代吗?不是的话,产生了什么问题?

答:不是。HotSpot使用永久代的逻辑设计来实现方法区,其目的是为了将方法区的内存收集纳入堆的垃圾收集器收集范围中,而比较JRockit 和 J9,它们都没有将方法区纳入垃圾收集区域中,好处是方法区没有默认的maxPermSize,不容易发生内存溢出(理论上直到触碰到进程分配的内存上限前(32位系统是4g))。根据方法区实现逻辑的差异,一些特殊的方法(String::intern())调用表现也就有所不同。由于HotSpot方法区的实现逻辑与JRockit的差异,前者在融合后者的特性时,遇到了诸多阻碍,因此从JDK7移除了部分永久代的字符常量池和静态变量(此时还保留了类型信息),到JDK8最终完全移除了永久代,采用本地内存的元空间代替。

问:什么是运行时常量池?

答:运行时常量池是方法区的一部分。《Java虚拟机规范》对其没有任何细节要求。大体上,Class文件的常量池表用于存放编译期生成的各种字面量和符号引用,在类加载时,会存放到运行时常量池。

此外,符号引用翻译出来的直接引用也会存储在这个区域。

运行时常量池具有动态性,在运行期间,也可以将新的常量放入池中(例如String::intern())

问:Class文件包含了什么内容?

答:类的版本、字段、方法、接口等描述信息,还有存放编译期生成的字面量和符号引用的常量池表。

问:什么是直接内存?

答:直接内存不是虚拟机内存的一部分,《Java虚拟机规范》对其也没有涉略,但在JDK1.4中加入NIO后,这种新型的基于通道和缓冲区的IO方式可以通过调用新增的native函数库来 *** 作堆外内存,然后通过Java堆中的DirectByteBuffer对象作为该堆外内存的引用进行 *** 作,从而减少IO传输过程中从Native堆到Java堆中的复制开销。这时,该申请的堆外内存称为直接内存。

直接内存受本机总内存、寻址空间影响,不受虚拟机进程内存影响。

二、HotSpot虚拟机对象创建始末

虚拟机对象数据如何创建、如何布局、如何访问?(以HotSpot为例)

问:对象是如何创建的?

答:程序计数器随线程执行到方法而初始化,若方法是字节码,则存储指示命令行的地址,当执行到new关键字时,虚拟机会检查new指令参数(也就是创建的类型)在运行时常量池中是否有对应的符号引用(类型信息)且是否已经被加载(初始化),若都没有,则会进行类加载。

在类加载检查之后,虚拟机会根据加载后计算好的内存大小,为对象在Java堆中分配内存,分配的方式分为“指针碰撞”和“空闲列表”,视Java堆的规整程度而定。收集器的空间压缩整理能力决定Java堆的规整能力。

内存分配完毕后,虚拟机会对内存空间进行初始化(数据类型的默认值,例如int a = 0,实例则初始化为0),以保证通过编译期检查(Java程序中不需要对对象的实例字段进行赋值便可以进行引用),若激活了TLAB设置,则线程在预分配TLAB内存时,便已经提前进行了内存空间的初始化了。

初始化完毕后,会对对象进行信息设置,设置对象头(类信息,类的元数据地址,对象的哈希值(真正调用Object::hashCode()方法时才计算),对象GC分代年龄等)、实例数据、对象填充。

信息设置完成,则虚拟机层面的对象创建完成,但Java层面的对象尚未完成,此时将执行对象的构造函数,走init命令,执行invokespecial指令,进行对象资源信息的填充。

填充完毕后,Java对象创建完毕。

PS:Java编译器在编译new关键字时,会生成new指令和invokespecial指令。

eg:Serial、ParNew具备,而CMS理论上不具备(内部划分的各个缓冲区具备)

问:什么是分配内存时的“指针碰撞”和“空闲列表”?

答:在Java堆中,以一个指针为临界点,区分正被使用内存以及空闲内存,指针碰撞便是创建对象,挪动指针的过程;指针碰撞要求堆中的内存规整,在已被使用内存和空闲内存相互掺杂的区域则无法进行,此时使用“空闲列表”——在列表中记录何处有多大的空闲内存,分配时从列表查询,并进行更新。

问:关于对象创建,在并发情况下可能产生什么问题?解决方法是什么?

答:堆中对象的创建过程很频繁,此时,不同的引用指针指向的内存空间是可能出现并发安全问题的。

解决方法一:对分配对象的过程进行同步处理。虚拟机通过调用系统的CAS指令进行内存分配,失败则重试(《Java高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计模式》那本书有更详细的解释)。

方法二:为每个线程在堆中预分配TLAB内存(Thread Local Allocation Buffer本地线程分配缓冲区),创建对象时在该内存中进行,当TLAB不足或用完,需要重新分配TLAB内存时,才用CAS方式处理。

-XX: +/-UseTLAB
问:对象的内存是如何布局的?

答:对象的内存布局包含对象头、实例数据、对齐填充三部分。

问:对象的对象头主要包含什么内容?

答:主要包含对象运行时自身数据、元数据类型指针、数组长度数据。

对象运行时元数据:哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向标志(详细可见《Java高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计模式》)

元数据类型指针(根据虚拟机的实现来,有的不一定存在):对象通过该指针,在方法区中找到对象是哪个类的实例(主要作用通过类确定对象的大小)。

数组长度数据:当对象代表一个数组时,由于元数据无法确定数组长度,所以需要做记录。

问:对象的实例数据部分存储什么内容?

答:对象真正的有效信息。包括子类自身的以及从父类继承下来的各字段内容。记录的顺序受到虚拟机参数-XX: FieldsAllocationStyle 和字段在源码中的排序的影响。

字段排序上,虚拟机默认分配的顺序是相同宽度的字段排在一起(longs/doubles,ints,shorts/chars,bytes/booleans,oops),其次,再优先将父类继承下来的字段排列。

PS:若HotSpot虚拟机参数+XX:CompactFields值为true,则子类的窄变量也会允许插入父类变量的空袭之中,以节省空间。

问:对象中的对齐填充是什么?

答:对齐填充本身不具备意义,但HotSpot虚拟机自动内存管理系统要去任何对象的起始地址都必须是8字节的整数倍,虽然对象头必定满足8的整数倍(32bit或64bit),但实例数据区无法保证,因此产生对齐填充。

问:如何访问对象?

答:源码中,我们创建对象后常常会声明一个变量对其进行指向。这个变量称为引用(reference)。这个引用会存储在栈帧的局部变量表中,访问对象便是这个引用访问对象的过程。

有两种定位访问对象的方式:句柄和直接引用。

句柄是在堆中会划出一块内存作句柄池,句柄池中每个句柄包含了对象的实例数据地址和类型数据地址,reference则存储句柄池中对应的句柄地址。对象移动后,更改句柄池的实例数据指针即可,而reference不需要更改。

直接引用:reference直接存储堆中的对象实例数据地址,对象实例需要存储类型数据地址(此时对象头存储类型指针)

HotSpot采用的是直接引用(Shenandoah收集器需要额外一次转发)。

三、OutOfMemoryError 问:内存溢出和内存泄露的区别是什么?

答:

内存溢出(out of memory):申请时内存不足,申请失败。

内存泄露(memory leak):申请完的内存无法释放。

问:怎么设置Java堆大小?

答:在vm参数中设置:

-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError表示当出现内存溢出时,Dump出当前的内存堆转储快照。

问:怎么分析Java堆的内存溢出异常?

答:单独博客分析。

问:怎么设置虚拟机栈和本地方法栈大小?

答:虚拟机栈大小设置参数:-Xss

本地方法栈大小设置参数:-Xoss

PS:HotSpot的本地方法栈和虚拟机栈是融合一起的,所以-Xoss无效。

问:虚拟机栈可能抛出什么异常?

答:若请求栈深度大于max深度值,或栈大小大于栈容量,将抛出stackOverflowError,有错误堆栈可分析。

若栈允许动态扩展,则当扩展容量不能满足申请内存时,抛出OutOfMemoryError。

PS:HotSpot不支持栈的动态扩展。

问:虚拟机栈的最小值和最大值为多少,怎么理解?

答:虚拟机栈的最小值和最大值受 *** 作系统和虚拟机两者的影响。

32位Windows、JDK6:最小值128k;

64位Windows、JDK11:最小值180k;

64位Linux、JDK11:最小值228K;

问:Hopspot虚拟机下,线程在什么情况下能出现OutOfMemoryError?

答:线程的创建在进程中进行,若线程容量设置很大,在大量创建线程情况下,可能出现OutOfMemoryError。

在32位Windows系统下,单个进程最大内存为2G,更容易出现本异常。

问:32位的系统,在不能进行升级和提升机器性能的情况下,怎么降低频繁创建线程时出现OutOfMemoryError的概率?

答:降低最大堆容量或栈容量来实现。

问:String::intern()方法的作用是什么?在永久代和元数据空间之中,有什么区别?

答:使用String::intern()方法是Native方法,若字符串在字符串常量池中存在,则直接返回该String对象,否则先在字符串常量池中添加该String对象,再返回它。若在JDK6版本及以前使用,当永久代内存不足时,可抛出OutOfMemory ERROR,但此后伴随字符串常量池迁移到堆以及永久代的完全废除,运行时常量池几乎不可能(除非堆只有几MB)出现该异常(不足时会回收)。

问:JDK6及以前的永久代设置参数是多少?

答:

-XX:PermSize=6M -XX:MaxPermSize=6M
问:在JDK6以JDK8下,下列代码的执行结果分别是什么?为什么?

答:

public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }

JDK6:false、false。

原因:str1对象和str2对象在堆中实例化,分别调用intern时,返回的是方法区中字符对象的引用地址,与堆中的引用地址不相同。

JDK8:true、false。

原因:str1对象在堆中实例化后,调用intern方法时,由于该字符不存在于运行时常量池,所以会在运行时常量池中记录该实例地址,并将其返回。所以两个相同的实例地址比较地址,结果相等。

而str2对象在堆中实例时,调用intern方法,发现该字符已经在运行时常量池存在,所以str2.intern()返回的实例地址并非str2的实例地址,结果不相等。

问:在实际生产中,什么因素影响方法区?

答:大量的框架(spring)通过动态代理的方式产生大量动态类,这些动态类会占用方法区。当生成的动态类越多,就越需要更大的方法区。此外,一些应用也会产生大量的动态类:JSP(第一次编译时会被编译为Java类),我们需要关注方法区的内存溢出,因为类的垃圾回收条件是比较苛刻的。

问:在JDK8及以后,设置元空间大小的参数是什么?

答:

-XX:MaxmetaspaceSize
    默认-1,表示不限制,只受本地内存大小影响
    
-XX:metaspaceSize
    指定元空间初始化大小,数据占用达到该大小时,会启动垃圾收集器进行收集,若释放空间小,则会自动适当提升该值(不大于max情况下),反之,会自动降低该值。
    
XX:MinmetaspaceFreeRatio
    指定垃圾收集后最小的元空间剩余容量占比,防止metaspaceSize太小,也可以减少频繁的垃圾回收。
有-XX:Max-metaspaceFreeRatio
    指定垃圾收集后最小的元空间剩余容量占比,防止metaspaceSize太大,造成空间浪费。
问:什么是直接内存溢出?

答:直接内存默认大小与最大堆大小一致,可通过参数-XX: MaxDirectMemorySize进行指定。一般地,直接内存的申请和分配是由Unsafe实例进行 *** 作的,在JDK10以前,除非使用反射,否则只有在虚拟机标准类库(例如NIO)中才能使用Unsafe功能(Unsafe::allocateMemory())。当向直接内存申请分配内存时,虚拟机经过计算,会提前发现内存不足,并在申请动作发生前,抛出OutOfMemoryError异常。

PS:直接内存抛出异常时,Heap Dump文件不会有明显特征,且Dump文件很小。用户需留意是否程序中直接或间接使用了直接内存(例如NIO)。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5677496.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存