jvm堆概述
2021/7/14 23:49:28
本文主要是介绍jvm堆概述,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
堆
1.堆的核心概述
一个进程对应一个jvm实例,一个jvm实例只有一个运行时数据区,有多个线程共享同一个堆,每个线程有私有的程序计数器,本地方法栈,虚拟机栈
-
一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域。
-
java堆区在jvm启动时就被创建,其空间大小也就确定了,是jvm管理的最大一块内存空间
堆内存大小是可以调节的
-
《java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为是连续的
-
所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(thread local allocation buffer, tlab)
一个进程一个堆验证
package com.cxf.heap; /** * -Xms=10m * -Xmx=10m */ public class HeapDemo { public static void main(String[] args) { System.out.println("start"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); } }
package com.cxf.heap; /** * -Xms=20m * -Xmx=20m */ public class HeapDemo1 { public static void main(String[] args) { System.out.println("start"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); } }
如图所示区域加起来是等同于我们我们设置的堆大小
-
《java虚拟机规范》中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
“几乎”所有的对象实例都会在这里分配内存—从实际使用角度来看
-
数组和对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
-
在方法结束后,堆中的对象不会马上移除,仅仅在垃圾收集的时候才会被移除
-
堆是gc执行垃圾回收的重要区域
字节码指令中每一次的new都会创建对象实例,并在堆中开辟空间
堆的核心概述:内存细分
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
2.设置堆内存的大小与oom
-
java堆区用于存储Java对象实例,那么堆的大小在jvm启动时就已经设定好了,大家可以通过选项“-Xms”和“-Xmx”来进行设置
-Xms用于表示堆的起始内存,等价于-XX:InitalHeapSize
-Xmx用于表示堆的最大内存,等价于-XX:MaxHeapSize
-
一旦堆区中的内存大小超过-Xmx所指定的内存就会抛出OutofMemoryerror异常
-
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
-
默认情况下:初始内存大小:物理电脑内存/64
最大内存大小:物理电脑内存大小/4
OOM的举例说明
示例代码及参数
package com.cxf.heap; import java.util.ArrayList; import java.util.Random; /** * -Xms600m -Xmx600m */ public class HeapSpaceOomTest { public static void main(String[] args) { ArrayList<pictures> pictures = new ArrayList<>(); // long l = Runtime.getRuntime().maxMemory(); // System.out.println(l/1024/1024+"M"); while (true){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } pictures.add(new pictures(new Random().nextInt(1024*1024))); } } } class pictures{ private byte[] piexs; public pictures(int length){ this.piexs=new byte[length]; } }
3.年轻代与老年代
-
存储在jvm中的java对象可以被划分为两类
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
另外一类的对象的生命周期却非常长,在某些极端的情况下甚至能和jvm的生命周期保持一致
-
java堆区进一步细分的话,可以划分为年轻代(youngGen)和老年代(oldGen)
-
其中年轻代又可以划分为eden空间、survivor0空间和survivor1空间(有时也叫做from区、to区)
下边这参数开发中一般不会调
-
配置新生代与老年代在堆结构的占比
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
package com.cxf.heap; /** * -Xms600m -Xmx600m */ public class test9 { public static void main(String[] args) { System.out.println("打酱油"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
内存比例我们可以通过jvisualvm、jps jstat -gc pid、jps jinfo -flag newratio pid 来查看新生区和老年区的内存比例
-
在hotspot中,eden空间和另外survivor空间缺省所占的比例是8:1:1
-
当然开发人员可以通过选项-XX:SurvivorRation 调整这个空间比例。比如-XX:SurvivorRatio=8
-
几乎所有的java对象都是在eden区被new出来的
-
绝大部分的java对象的销毁都在新生代进行了
ibm公司的专门研究标明,新生代中80%的对象都是朝生夕死的
-
可以使用选项“-Xmn”设置新生区最大内存大小
这个大小一般使用默认值就好
默认是有一个自适应的机制
所以上边打酱油的那个程序你在查看堆内新生区的内存分配的时候斌并不是按我们上述的8:1:1而是6:1:1
可以使用-XX:-UseAdaptiveSizePolicy 来关闭自适应的机制
发现没有用 哈哈哈哈嗝
还是得用
-XX:SurvivorRatio=8 手动设置比例
在运行时就是默认的8:1:1了
4.图解对象分配过程
为新对象分配内存是一件非常严谨和复杂的任务,jvm的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和垃圾回收算法密切相关,所以还需要考虑gc执行完内存回收后是否会在内存空间中产生内存碎片
1.new的对象先放到伊甸园区,此区有大小限制
2.当伊甸园区的空间填满时,程序又需要创建对象,jvm的垃圾回收器将对伊甸园区进行垃圾回收,minor gc,将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区
3.然后将伊甸园区中的剩余对象移动到幸存者0区
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
6.啥时候可以去养老去呢?可以设置次数,默认是15次
- 可以设置参数:-XX:MaxTenuringThreshold=进行设置
7.在养老区,相对悠闲。当养老区内存不足时,再次触发GC:major GC,进行养老区的内存清理
8.若养老区执行了major Gc之后发现依旧无法执行对象的保存,就会产生oom
每个对象分配一个年龄计数器,从伊甸园区到幸存者0区,年龄计数器会变为1
此时伊甸园区没有被gc的会被放在幸存者1区
幸存者0区的对象也会被判断是否被回收
如果没有被回收的话,则也会移动到幸存者1区,同时年龄计数器++
幸存者0区,和幸存者1区同时也被称为from、to,但这个from、to是不确定的
比如经过上图的gc过程后,幸存者0区就是to区,而幸存者1区就是from区
即每次执行完gc之后,哪个幸存者区是空的,哪个就是to区
to区即伊甸园区执行完gc之后,剩余对象往哪放
从幸存者区晋升到老年区才会用到年龄计数器
- 幸存者区满了之后不会触发YGC!!!!!!!!!!
- 但在YGC触发时,幸存者区会被动的GC
- 也有可能一被创建就在老年代
- 也有可能年龄计数器没有达到阈值就到达老年代
- 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/原空间收集
对象分配特殊情况
1.当新对象进行创建的时候,先会判断伊甸园区是否放得下,如果放的下的话就直接进行内存分配,如果放不下就会进行YGC
- 进行ygc时会判断servivor是否放的下,放的下的话就放在幸存者0或1区,放不下的话直接晋升老年代
- 其次会判断对象的年龄计数器是否超过阈值,超过的话直接晋升老年代,否则还是放置在幸存者0或1区
2.在判断伊甸园区是否放得下,如果伊甸园区放的下就直接分配对象内存,如果还是放不下,就判断老年区是否放的下,如果老年区放得下就直接再老年区分配对象内存,如果老年区也放不下先对老年区进行fgc类似于major gc ,之后再判断老年区是否放的下,放的下的话直接分配对象内存,放不下的话直接抛出oom
package com.cxf.heap; import java.util.ArrayList; import java.util.Random; /** * -Xms600m -Xmx600m */ public class HeapInstanceTest { byte[] buffer=new byte[new Random().nextInt(1024*1024)]; public static void main(String[] args) { ArrayList<HeapInstanceTest> buffers = new ArrayList(); while(true){ buffers.add(new HeapInstanceTest()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
常用的调优工具
5.minor GC、major GC、Full GC
minor gc=ygc
针对老年代的gc即major gc
jvm在进行gc时,并非每次都对上边三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代
针对hotspot vm的实现,它里边的gc按照回收区域又分为两大种类型:一种是部分收集,一种是整堆收集
- 部分收集:不是完整收集整个java堆的垃圾收集。其中又分为
1.新生代收集(minor gc/young gc):只是新生代(eden\s0,s1)的垃圾收集
2.老年代收集(major gc/old gc):只是老年代的垃圾收集
目前只有,cms gc会有单独收集老年代的行为
注意,很多时候major gc会和full gc混淆使用,需要具体分辨是老年代回收还是整堆回收
3.混合收集mixed gc:收集整个新生代以及部分老年代的垃圾收集
目前只有g1 gc会有这种行为
-
整堆收集(FULL gc):收集整个java堆和方法区的垃圾收集
-
年轻代gc(minor gc)触发机制
- 当年轻代空间不足时,就会触发minor GC,这里的年轻代指的是eden代满,survivor满不会引发gc,(每次minor gc会清理年轻代的内存)
- 因为java对象大多具备朝生夕灭的特性,所以minor gc非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解
- minor gc会引发stw,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
-
老年代GC(major gc/full gc)触发机制
- 指发生在老年代的gc,对象从老年代消失时,我们说major gc或full gc发生了
- 出现了major gc,经常会伴随至少一次的minor gc(但并非绝对的,再parallel scavenge)收集器的收集策略里就会有直接进行major gc的策略选择过程
- 也就是再老年代空间不足时间会先尝试进行minor gc,如果之后空间还是不足则触发major gc
- major gc的速度一般会比minor gc慢10倍以上,stw的时间更长
- 如果major gc后,内存还不足,就报oom了
-
full gc触发机制
-
触发full gc执行的情况有如下五种:
- 调用system.gc()时,系统建议执行full gc,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过minor gc后进入老年代的平均大小大于老年代的可用内存
- 由eden区、survivor space0(from space)区向survivor space1(to space)区复制时,对象大于to space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象的大小
说明:full gc是开发或调优中尽量要避免的,这样暂时时间会短一些
package com.cxf.GC; import java.util.ArrayList; /* *体会minor gc、major gc、full gc */ public class GcTest { public static void main(String[] args) { int i=0; try { ArrayList<String> list = new ArrayList<>(); String a="cxfszz"; while(true){ list.add(a); a=a+a; i++; } } catch (Exception e) { e.printStackTrace(); System.out.println("遍历次数为"+i); } } }
6.堆空间分代思想
为什么需要把java堆分代?不分代就不能正常工作了嘛?
- 经研究,不同对象的生命周期不同,70%-99%的对象是临时对象
- 新生代:由eden、两块大小相同的幸存者区构成,to总为空
- 老年代:存放新生代中经历多次gc仍然存活的对象
- 其实不分代完全可以。分代的唯一理由就是优化gc性能。如果没有分代,那所有的对象都在一块,那就如同把一个学校的人都关在一个教室。gc的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描,而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放在某一个地方,当gc的时候先把这块存储朝生夕死对象的区域进行回收,这样就会腾出很大的空间出来
7.内存分配策略
如果对象再eden出生并经过第一次minorgc 后任然存活,并且能被survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1.对象在survivor区每熬过一次minorgc,年龄就增加1岁,当他的年龄增加到一定程度,(默认为15岁,其实每个jvm、每个gc都有所不同)时,就会被晋升到老年代中。
对象晋升到老年代的年龄阈值,可以通过选项-XX:MaxTenuringThreshold来设置年龄的阈值
内存分配策略
-
优先分配对象到eden
-
大对象直接分配到老年代
-
尽量避免程序中出现过多的大对象
就如上边对象分配的特殊原则,会进行ygc在判断eden是否放得下,放不下再去判断老年代是否放得下,然后再在老年代中为对象分配内存,倘若老年代放不下的话,还要进行major gc
,进行gc的时候会触发stw,倘若放在老年代的这个对象朝生夕死的话就会很浪费
-
-
长期存活的对象分配到老年代
-
动态年龄判断
- 如果survivor区中形同年龄的所有对象大小总和大于survivor空间的一般,那么年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到所设置的阈值
-
空间分配担保
-
-XX:HandlePromotionFailure
在eden进行完minor gc之后如果剩余的对象过多,无法全部放入survivor区剩余的会直接放入老年代
-
8.为对象分配内存TLAB
为什么有TLAB(thread local allocation buffer)?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在jvm中非常的频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
什么是tlab?
- 从内存模型而不是垃圾收集的角度,对eden区域继续进行划分,jvm为每个线程分配了一个私有缓存区域,它包含在eden空间内
- 多线程同时分配内存时,使用tlab可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略
- 据我所知所有openjdk衍生出来的jvm都提供了tlab的设计
tlab的再说明:
-
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但Jvm确实是将Tlab作为内存分配的首选
-
在程序中,开发人员通过选项“-XX:UseTLAB”设置是否需要开启Tlab空间,默认情况下该参数是开启状态
package com.cxf.GC; /** * 默认情况下-XX:UseTLAB 是开启的状态 */ public class TlabArgsTest { public static void main(String[] args) { System.out.println("打酱油"); try { Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
- 默认情况下,tlab空间的内存非常小,仅占有整个eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用eden空间的百分比大小
- 一旦对象在tlab空间分配内存失败时。jvm就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在eden空间中分配内存
9.小结堆空间的参数设置
package com.cxf.GC; /** * 此时堆空间常用的jvm参数: * -XX:+PrintFlagsInitial:查看所有的参数的默认初始值 * -XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值) * -Xms:初始堆空间内存 默认为物理内存的1/64 * -Xmx:最大堆空间内存(默认为物理内存的1/4) * -Xmn:设置新生代的大小(初始值及最大值) * -XX:NewRatio:配置新生代与老年代在堆结构中的占比 * -XX:SurvivorRatio:设置新生代中eden和s0/s1的空间占比 * -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄 * -XX:+PrintGCDetails:输出详细的GC处理日志 * 打印gc简要信息 * 1.-XX:+PrintGC 2.-verbose:gc * -XX:HandlePromotionFailure:是否设置空间担保 * */ public class HeapArgsTest { }
-XX:HandlePromotionFailure:是否设置空间担保
jdk7及以后该jvm参数有所变化及当老年代的最大可用的连续空间大于新生代的所有对象的总和或者大于历次晋升老年代的平均大小就进行minor gc否则则进行 full gc
在发生minor gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总和
- 如果大于,则此次minor gc是安全的的
- 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
- 如果为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小
- 如果大于,则尝试进行一次minor gc,但这次minor gc任然是有风险的
- 如果小于,则改为进行一次full gc
- 如果为false,则改为进行一次full gc
- 如果为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小
X.堆时分配对象的唯一选择嘛
根据前边的学习,大多数情况下新创建的数组和对象都会在堆中创建
堆是分配对象存储的唯一选择吗?
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段
- 这是一种可以有效减少java程序中同步负载和内存压力的跨函数全局数据流分析算法
- 通过逃逸分析,java hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象动态作用域
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
- 当一个对象在方法中被定义后,他被外部方法所引用,则认为发生逃逸。例如调用参数传递到其他地方中
结论:开发中能使用局部变量的,就不要使用在方法外定义
使用逃逸分析,编译器可以对代码做如下优化:
一:栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
package com.cxf.GC; /** * -Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:PrintGCDetails */ public class EscapeAnalysisTest { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <10000000 ; i++) { getUser(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } private static void getUser(){ user user = new user(); } } class user{ }
package com.cxf.GC; /** * -Xms1G -Xmx1G -XX:+DoEscapeAnalysis -XX:PrintGCDetails */ public class EscapeAnalysisTest { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <10000000 ; i++) { getUser(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } private static void getUser(){ user user = new user(); } } class user{ }
同样的代码当我们把堆内存的设置减少到256m时
不开启逃逸分析的运行结果如下,可以看到发生了gc
而开启了逃逸分析的相同代码,则没有发生gc
二:同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以考虑不同步
- 线程同步的代价是相当高的,同步的后果是降低并发性和性能
- 在动态编译同步块的时候,jit编译器可以借助逃逸分析来判断同步块所使用的锁是否只能被一个线程访问而没有发布到其他线程。如果没有,那么jit编译器在编译这个同步块的时候就会取消对这部分代码的同步,这样就能大大提高并发性和性能,这个同步的过程就叫同步省略,也叫作锁消除
同样的同步省略并不是在编译器进行的,而是在运行 的时候进行的
通过观察反编译过后的字节码文件我们就能看出
三:分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或者全部可以不存储在内存,而是存储在cpu寄存器中
标量(scalar)是指一个无法在分解成更小的数据的数据。java中的原始数据类型就是标量(比如基本数据类型),相对的那些还可以分解的数据叫做聚合量(Aggregate),java中的对象就是聚合量,因为他可以分解为其他聚合量和标量
在jit阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过jit优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换
代码示例
package com.cxf.GC; /** * -Xms60m -Xmx60m -XX:-EliminateAllocations -XX:+DoEscapeAnalysis -XX:+PrintGCDetails */ public class ScalarReplaceTest { public static class user{ public int id; public String name; } public static void allow(){ user user = new user();//未发生逃逸 user.id=18; user.name="cxf"; } public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <1000000 ; i++) { allow(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); } }
package com.cxf.GC; /** * -Xms60m -Xmx60m -XX:+EliminateAllocations -XX:+DoEscapeAnalysis -XX:+PrintGCDetails */ public class ScalarReplaceTest { public static class user{ public int id; public String name; } public static void allow(){ user user = new user();//未发生逃逸 user.id=18; user.name="cxf"; } public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <1000000 ; i++) { allow(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); } }
这篇关于jvm堆概述的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-082024年常用的情绪识别API
- 2025-01-07如何利用看板工具优化品牌内容创作与审批,确保按时发布?
- 2025-01-07百万架构师第十一课:源码分析:Spring 源码分析:Spring源码分析前篇|JavaGuide
- 2025-01-07质量检测标准严苛,这 6 款办公软件达标了吗?
- 2025-01-07提升品牌活动管理的效率:看板工具助力品牌活动日历的可视化管理
- 2025-01-07宠物商场的精准营销秘籍:揭秘看板软件的力量
- 2025-01-07“30了,资深骑手” | 程序员能有什么好出路?
- 2025-01-07宠物公园的营销秘籍:看板软件如何帮你精准触达目标客户?
- 2025-01-07从任务分解到资源优化:甘特图工具全解析
- 2025-01-07企业升级必备指南:从传统办公软件到SaaS工具的转型攻略