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)。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)