当前位置: 首页 > 后端技术 > Java

【无为原创】4万字图文详解java堆内存与OOM解决方案

时间:2023-04-01 13:44:17 Java

这篇文章大概看了10分钟,内容如下:什么是JVM堆?Java对象是不是都放在堆上了?线程和堆的关系堆的内部结构面试题新生代和老年代如何设置堆的Size?新生代与老年代的比例设置Eden和survivor的比例常用参数对象分配黄金句:分配过程内存分配策略(或对象提升(promotion)规则):对象分配原则MinorGC,MajorGC,FullGCMinorGC触发机制OldgenerationGC(MajorGC/FullGC)触发机制:FullGC触发机制:如何解决OOM为什么要对Java堆进行分代?不分代就不能正常工作吗?什么是TLAB(快速分配策略)?为什么会有TLAB(ThreadLocalAllocationBuffer)快速分配策略?什么是TLAB?TLAB说明:什么是JVM堆一个JVM实例只有一块堆内存,堆也是Java内存管理的核心区域。Java堆区是在JVM启动时创建的,它的空间大小是确定的。它是JVM管理的最大的内存空间。堆内存的大小可以调整。《Java虚拟机规范》规定堆可以在物理上不连续的内存空间,但逻辑上应该认为是连续的。堆是GC(GarbageCollection,垃圾收集器)进行垃圾回收的关键区域。方法结束后,堆中的对象不会立即被移除,只会在垃圾回收时被移除。Java对象是不是都放在堆上了?《Java虚拟机规范》中对Java堆的描述是:所有的对象实例和数组在运行时都应该分配在堆上。(堆是运行时数据区域,从中分配所有类实例和数组的内存)数组和对象可能永远不会存储在堆栈中,因为堆栈帧在堆的位置保存对对象或数组的引用。我想说的是:“几乎”所有对象实例都在这里分配内存。——从实际使用的角度来看。示例:publicclassSimpleHeap{privateintid;publicSimpleHeap(intid){this.id=id;}publicvoidshow(){System.out.println("我的ID是"+id);}publicstaticvoidmain(String[]args){SimpleHeapsl=newSimpleHeap(1);SimpleHeaps2=newSimpleHeap(2);}}线程和堆的关系所有线程共享Java堆,这里也可以划分线程私有缓冲区(ThreadLocalAllocationBuffer,TLAB)。堆的内部结构大多数现代垃圾收集器都是基于分代收集理论设计的。堆空间细分为:Java7及之前堆内存在逻辑上分为三部分:新生区+老年代区+永久区YoungGenerationSpaceYoung/New分为Edenarea和SurvivorareaTenuregenerationspaceOld/TenurePermanentSpacePermanentareaPermJava8及之后的Heapmemory在逻辑上分为三部分:Newbornarea+oldagearea+metaspaceYoungGenerationSpaceYoungGenerationSpaceNewbornareaYoung/New分为Edenarea和SurvivorareaTenuregenerationspaceOld/TenureMeta空间元空间元协议:新生代区<=>新生代<=>年轻代退休区<=>老年区<=>老年代永久区<=>永久代理面试题Java堆的结构是什么样的?(猎聘)JVM内存为什么要分新生代、老年代、永久代。为什么新生代在Eden和Survivor(字节跳动)堆中分为分区:Eden、survivor(from+to)、oldgeneration,以及各自的特点。(JD-Logistics)堆的结构?为什么有两个幸存者区?(蚂蚁金服)Eden和Survivor比例分配(蚂蚁金服)JVM内存分区,为什么会有新生代和老年代(小米)JVM内存结构,Eden和Survivor比例。(京东)JVM内存为什么要分新生代、老年代、永久代。为什么新生代分为Eden和Survivor。(京东)JVM内存分区,为什么要有新生代和老年代?(美团)JVM的内存结构,Eden和Survivor的比例。(京东)新生代和老年代JVM中存储的Java对象可以分为两类:一类是瞬态对象,生命周期很短,这类对象的创建和消亡都非常快;另一类对象虽然有生命周期,但是很长,在某些极端情况下,可以和JVM的生命周期保持一致。如果Java堆区进一步细分,可以分为年轻代(YoungGen)和老年代(OldGen)。新生代可分为Eden空间、Survivor0空间和Survivor1空间(有时也称为fromarea,toarea)。几乎所有的Java对象都是在Eden区新建的。Java对象的大部分销毁是在新生代中进行的。IBM专项研究表明,新生代中80%的对象都是“生死存亡”。如何设置堆的大小?新生代与老年代的比例配置了新生代和老年代在堆结构中的比例。默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3。可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/3。占整个堆的1/5,可以使用选项“-Xmn”来设置新生代的最大内存大小。该参数一般使用默认值。设置Eden和Survivor的比例在HotSpot中,Eden空间和另外两个Survivor空间的默认比例是8:1:1。当然,开发者可以通过选项“-XX:SurvivorRatio”来调整这个空间比例。例如-XX:SurvivorRatio=8常用参数堆空间大小的设置:-Xms:初始内存(默认为物理内存的1/64);-Xmx:最大内存(默认为物理内存的1/4);设置新生代的大小。(初始值和最大值)-Xmn通常是默认值。配置新生代和老年代在堆结构中的比例。分配的值是老年代占的比例,剩下的1分给新生代默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5。在HotSpot中,Eden空间与另外两个Survivor空间的默认比例是8:1。“-XX:SurvivorRatio”调整这个空间比例。例如-XX:SurvivorRatio=8设置了新生代垃圾的最大年龄。如果超过这个值还没有被回收,就会进入老年代。默认值为15-XX:MaxTenuringThreshold=0:表示新生代对象直接进入老年代,不经过Survivor区。对于有很多老年代的应用程序,可以提高效率。如果该值设置较大,新生代对象会在Survivor区被多次复制,可以增加对象在新生代的存活时间,增加在新生代被回收的概率。输出详细的GC处理日志-XX:+PrintGcDetail-XX:HandlePromotionFailure在MinorGC发生前,虚拟机检查老年代最大可用连续空间是否大于新生代所有对象的总空间。如果大于,则MinorGC是安全的,如果小于,虚拟机会检查-XX:HandlePromotionFailure的设置值是否允许保证失败。如果HandlePromotionFailure=true,那么它会继续检查老年代的最大可用连续空间是否大于提升到老年代的对象的平均大小。如果较大,尝试进行一次MinorGC,但是这次MinorGC还是有风险的;如果小于或HandlePromotionFailure=false,则改为执行FullGC。----------------------JDK6Update24后,HandlePromotionFailure参数将不再影响虚拟机的空间分配保证策略。观察OpenJDK中的源代码变化。虽然在源代码中定义了HandlePromotionFailure参数,但代码中将不再使用。JDK6Update24之后的规则变成,只要老年代的连续空间大于新生代中对象的总大小或者之前提升的平均大小,就会进行MinorGC,否则进行FullGC.-XX:+PrintFlagsFinal:查看所有参数的最终值(可能有修改,不再是初始值)。具体查看一个参数的指令:jps:查看当前运行的进程jinfo-flagSurvivorRatio进程id对象被分配为为新对象分配内存是一项非常严谨和复杂的工作。JVM设计者不仅需要考虑如何分配内存、在何处分配内存,还需要考虑GC执行,因为内存分配算法与内存回收算法密切相关。内存回收后内存空间是否会产生内存碎片。金句:**幸存者s0和s1区总结:复制后有exchange,谁空谁to。关于垃圾回收:新区频繁回收,老区很少回收,永久区/元空间几乎不回收**分配过程1.新对象先放入伊甸园。该区域有大小限制。2.当Eden空间满了,程序需要重新创建对象。JVM垃圾回收器会对Eden区进行垃圾回收(MinorGC/YGC),回收Eden区中不再被其他对象引用的对象。破坏。然后将新的对象加载到Eden区3.然后将Eden区剩余的对象移动到survivor区0.4.如果再次触发垃圾回收,上次的survivor会被放到SurvivorZone0.如果没有被回收,它们会被放到Survivor1区。5.如果再经过垃圾回收,此时会放回Survivor0区,然后再去Survivor1区。6.什么时候可以去养老院?可以设置次数。默认为15次。可以设置参数:-XX:MaxTenuringThreshold=设置对象晋升到OldAge的年龄??阈值。7.在养老区,比较悠闲。当退休区内存不足时,再次触发GC:MajorGC,清理退休区内存。8.如果老年区执行MajorGC,发现对象无法保存,会产生OOM异常出生在Eden,如果经过第一次MinorGC后还活着,并且可以被Survivor容纳,则将其移至Survivor空间,并将对象年龄设置为1。对象在Survivor区每经历一次MinorGC存活,其年龄就会增加1年。当它的年龄增长到一定程度(默认是15岁,其实每个JVM和每个GC都不一样),就会被提升。进入老年。对象分配原则不同年龄段的对象分配原则如下:优先分配到伊甸园大对象直接分配到老年代尽量避免程序中大对象过多长期存活的对象分配到老年代oldage动态对象年龄判断如果Survivor区域内所有同年龄对象的大小之和大于Survivor空间的一半,年龄大于等于这个年龄的对象可以直接进入老年代无需等待MaxTenuringThreshold中要求的年龄。空间分配保证-XX:HandlePromotionFailure/**测试:大对象直接去老年代*-Xms60m-Xmx60m-XX:NewRatio=2-XX:SurvivorRatio=8-XX:+PrintGCDetails*/publicclassYoungOldAreaTest{publicstaticvoidmain(String[]args){byte[]buffer=newbyte[1024*1024*20];//20m}}MinorGC,MajorGC,FullGCJVM并不总是检查以上三种内存(newgeneration,老年代;方法区)一起回收,大部分时候回收是指新生代。HotSpotVM的实现,其中的GC根据回收区域分为两种:一种是部分收集(PartialGC),一种是全堆收集(FullGC)。Partialcollection:不是整个Java堆Garbagecollection的完整收集。又分为:MinorGC/YoungGC:只对新生代(Eden\S0,S1)进行垃圾回收Oldgenerationcollection(MajorGC/OldGC):只对老年代进行垃圾回收。目前只有CMSGC有单独收集老年代的行为。注意很多时候MajorGC会和FullGC混淆,需要具体区分是老年代收集还是full-heap收集。混合收集(MixedGC):收集整个新生代和部分老年代的垃圾收集。目前只有G1GC有这种行为全堆收集(FullGC):收集整个java堆和方法区的垃圾收集。MinorGC触发机制当年轻代空间不足时,会触发MinorGC。这里的younggenerationfull指的是Eden区full,Survivorfull不会触发GC。(MinorGC每次都会清理新生代的内存。)由于大多数Java对象都具有永恒的特性,所以MinorGC非常频繁,回收速度一般也比较快。这个定义清晰易懂。MinorGC会导致STW,挂起其他用户的线程,等待垃圾回收结束,用户线程才恢复运行。老年代GC(MajorGC/FullGC)触发机制:指发生在老年代的GC。当对象从老年代消失时,我们说发生了“MajorGC”或“FullGC”。MajorGC的出现,往往伴随着至少一次MinorGC(但不是绝对的,在ParallelScavenge收集器的收集策略中,有直接进行MajorGC的策略选择过程)。即当老年代空间不足时,会先尝试触发MinorGC。如果之后空间不够,触发MajorGCMajorGC的速度一般比MinorGC慢10倍以上,STW时间更长。如果MajorGC后内存不够用,就会报OOM。##FullGC触发机制:有五种情况会触发FullGC的执行:(1)调用System.gc()时,系统建议执行FullGC,但不一定执行(2)空格intheoldgenerationisinsufficient(3)MethodInsufficientareaspace(4)老年代通过MinorGC后的平均大小大于老年代的可用内存(5)从Eden区和survivorspace0复制时(FromSpace)区到survivorspace1(ToSpace)区,对象大小大于ToSpace可用内存,对象转移到老年代,老年代可用内存小于对象大小注意:开发或调优时尽量避免fullgc。这样会暂时缩短时间。publicclassOOMTest{publicstaticvoidmain(String[]args){Stringstr="www.atguigu.com";//把参数调小一点,这样问题会更早出现。//-Xms8m-Xmx8m-XX:+PrintGCDetailswhile(true){str+=str+newRandom().nextInt(88888888)+newRandom().nextInt(999999999);}}}/***测试MinorGC、MajorGC、FullGC*-Xms10m-Xmx10m-XX:+PrintGCDetails*/publicclassGCTest{publicstaticvoidmain(String[]args){inti=0;尝试{Listlist=newArrayList<>();Stringa="atguigu.com";while(true){list.add(a);一个=一个+一个;我++;}}catch(Throwablet){t.printStackTrace();System.out.println("遍历次数为:"+i);}}}如何解决OOM1、解决OOM异常或堆空间异常,一般的方法是先使用内存映像分析工具(如EclipseMemoryAnalyzer)进行堆转储,关键是确认对象是否存在内存中有必要,即区分是内存泄漏(MemoryLeak)还是内存溢出(MemoryOverflow)。2、如果是内存泄漏,可以通过工具进一步检查泄漏对象到GCRoots的引用链。然后你可以找出泄漏的对象如何与GCRoots相关联,并防止垃圾收集器自动回收它们。掌握了泄漏对象的类型信息和GCRoots引用链的信息后,可以更准确的定位泄漏代码的位置。3.如果没有内存泄漏,也就是说内存中的对象一定还活着,那么就应该检查虚拟机的堆参数(-Xmx和-Xms),并与虚拟机的物理内存进行比较机器,看看它们是否仍然可以调整。大,从代码上检查是否有一些对象的生命周期过长,保持状态时间过长,尽量减少程序运行时的内存消耗。为什么需要将Java堆分成几代?不分代就不能正常工作吗?其实不分代是完全可以的。生成的唯一原因是优化GC性能。如果没有生成,那么所有的对象都在一起,就像把一个学校的所有人都关在一个教室里一样。GC时,需要找出哪些对象是无用的,这样堆的所有区域都会被扫描。而很多对象是有生有死的,如果分代,把新创建的对象放在某个地方,GC的时候,先回收存放“生死”对象的区域,这样就会被释放.出来有很大的空间。什么是TLAB(快速分配策略)?为什么会有TLAB(ThreadLocalAllocationBuffer)快速分配策略?堆区是线程共享区,任何线程都可以访问堆区内的共享数据。由于在JVM中对象实例的创建非常频繁,在并发环境下将内存空间从堆区中划分出来是线程不安全的。为避免多个线程对同一个地址进行操作,需要使用加锁等机制,进而影响分配速度。因此,当多个线程同时分配内存时,使用TLAB可以避免一系列非线程安全问题,也可以提高内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略。什么是TLAB?从内存模型而非垃圾回收的角度,继续划分Eden区,JVM为每个线程分配一个私有缓存区,归入Eden空间。据我所知,所有从OpenJDK衍生出来的JVM都提供了TLAB设计。TLAB的说明:虽然不是所有的对象实例都能在TLAB中成功分配内存,但JVM确实将TLAB作为内存分配的首选。在程序中,开发者可以通过选项“-XX:+/-UseTLAB”来设置是否启用TLAB空间。默认情况下,TLAB空间中的内存很小,只占整个Eden空间的1%。当然,我们可以通过选项“-XX:TLABWasteTargetPercent”来设置TLAB空间占用Eden空间的百分比。一旦对象在TLAB空间分配内存失败,JVM会尝试使用锁机制保证数据操作的原子性,从而直接在Eden空间分配内存。连续三个码字好不容易求点赞收藏~