目录
6.1 堆的核心概述
6.2 设置堆内存大小与OOM
6.3 年轻代与老年代
6.4 图解对象分配过程
6.5 Minor GC,Major GC,Full GC
6.6 堆空间分带思想
6.7 内存分配策略
6.8 为对象分配内存:TLAB
6.8.1为什么有TLAB(ThreadLocal Allocation Buffer)
6.8.2 什么是TLAB?
6.9 小结堆空间的参数设置
10 堆是分配对象的唯一选择吗
11 总结
6.1 堆的核心概述
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了.是JVM管理的最大一块内存空间
- 堆的大小是可以调节的
- <<Java虚拟机规范>>规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该被视为连续的
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)
- 栈-堆-方法区的联系结构图
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
- 堆,是GC执行垃圾回收的重点区域
- 内存区分
- 内部结构
6.2 设置堆内存大小与OOM
- Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过"-Xmx","-Xms"来进行设置
- "-Xms":用于表示堆区的起始内存
- "-Xmx":用于表示堆区的最大内存
- 一旦堆区的内存大小超过"-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常.
- 通常会将"-Xmx"和"-Xms"这两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,避免在生产环境由于heap内存扩大或缩小导致应用停顿,降低延迟,从而提高性能.
- -Xms:堆内存的最小Heap值,默认为物理内存的1/64,但小于1G。默认当空余堆内存大于指定阈值时,JVM会减小heap的大小到-Xms指定的大小。
- -Xmx:堆内存的最大Heap值,默认为物理内存的1/4。默认当空余堆内存小于指定阈值时,JVM会增大Heap到-Xmx指定的大小。
- 设置完堆区大小后,实际的大小:伊甸园+一个幸存者区+老年代(另一个幸存者区是用来做复制算法的,并不计算这个的大小,但是会为其分配内存);伊甸园+一个幸存者区=新生代大小
- 查看设置的参数: jps / jstat -gc 进程id
- 查看设置的参数: -XX:+PrintGCDetails
- OutOfMemory举例
6.3 年轻代与老年代
- 生命周期长的放在不经常回收的老年代
- 新生代:伊甸园(对象最先创建的位置,后期进行回收时,发现里面有的对象已经死掉直接进行回收;有的对象仍在使用,把对象放在幸存者0区或者1区)
- 老年代和新生代的占比
- 查看新生代和老年代的占比
- jps / jinfo -flag NewRatio 进程号id
- 通过jvasual 可视化工具也可以查看
- 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例为8:1:1,默认开启自适应机制比例可能不是8:1:1而是6:1:1
- 当前开发人员可以通过选项"-xx:SurvivorRatio"调整这个空间比例.比如:-xx:SurvivorRatio=8,这个是显示设置并不会开启自适应机制
- 几乎所有的Java对象都是在Eden区被New出来的
- 绝大部分的Java对象的销毁都在新生代中进行
- 可以使用选项"-Xmn"设置新生代最大内存大小.
-
当-xx:NewRation和-xmn:同时出现时以-xmn(一般不进行设置)为准
6.4 图解对象分配过程
- 对象分配过程概述
- new的对象,先把他分配在伊甸园区,
- 伊甸园区满了之后,程序有需要创建对象,JVM的垃圾收集器将对伊甸园垃圾回收触发MinorGc可以判断谁是垃圾谁不是垃圾(回收伊甸园和幸存者区中的垃圾),不再被其他对象所引用的对象进行回收,将新的对象再加载到伊甸园中
- 剩下的被提升到幸存者0区(为每一个对象分配一个年龄计数器),
- 再次触发垃圾回收,把伊甸园中不是垃圾的对象放在幸存者1区(年龄计数器为1),并且判断幸存者0区中的对象是否仍在使用,若仍在使用提升到幸存者1区并且年龄计数器加1;伊甸园中的对象往空的幸存者区中存放;
- 重复上述步骤,当在幸存者区中年龄计数器达到15(阈值)时会晋升到老年代;幸存者区满的时候不会触发MinorGc;幸存者区在没有达到15时也可能进入老年代
- 老年带内存不足时,再次触发GC:Major GC,进行老年代的内存清理
- 若老年代执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常
可以设置参数:-XX:MaxTenuringThreshold=<N>进行设置
java.lang.outofmemoryError:Java heap space
- 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
- 关于垃圾回收:频繁在新生代收集,很少在老年代收集,几乎不再元空间收集
- 上述过程流程图
- 新生代放不下直接放到老年代(outofmemoryerror:实际上是老年带空间不足)
- 常用的调优工具
6.5 Minor GC,Major GC,Full GC
6.6 堆空间分带思想
6.7 内存分配策略
- 针对不同年龄段的分配原则如下所示:
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
- 空间分配担保
- -XX:HandlePromotionFailure
6.8 为对象分配内存:TLAB
6.8.1为什么有TLAB(ThreadLocal Allocation Buffer)
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一个地址,需要使用加锁等机制,进而影响分配速度
6.8.2 什么是TLAB?
-
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
-
多线程同时分配内存时 ,使用TLAB可以避免一系列的非县城安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略.
-
所有openJDK衍生出来的JVM都提供了TLAB的设计.
-
尽管不是所有的对象实例都能够在TLAB中成功分配到内存,但JVM确实是将TLAB作为内存分配的首选.
-
在程序中,开发人员可以通过选项"-XX:UseTLAB"设置是否开启TLAB空间
-
在默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项"-XX:TLABWasteTargetPercent"设置TLAB空间所占用Eden空间的百分比大小
-
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存.
- 对象的分配过程图解
- 流程:判断类有没有被加载过,如果被加载过,为该对象开辟空间优先考虑TLAB(当前线程在伊甸园中独占的一份内存)如果空间足够大话在TLAB中存放接着对象实例化;如果空间不够大,放入非TLAB的伊甸园中,如果空间仍不够大触发YGC空间仍然不够大直接进入老年代
-
默认情况下TLAB是开启的
6.9 小结堆空间的参数设置
- 官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
- -XX:PrintFlagsInitial:查看所有参数的默认初始值
- -XX:PrintFlagsFinal:查看所有参数的最终值(可能会存在修改)
- -Xms:初始堆空间内存(默认为物理内存的1/64)
- -Xmx:最大堆内存(默认为物理内存的1/4)
- -Xmn:设置新生代的大小(初始值即最大值)
- -XX:PrintFlagsInitial:查看所有参数的默认初始值
- -XX:NewRatio:配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio:设置新生代中Eden和S0和S1空间的比例
-
-xx:SurvivorRatio如果设置的不合理就会导致幸存者区变小幸存者去中的阈值未达到15就晋升到了老年代或者伊甸园中的对象直接就放到了老年代中(当相同年龄的所有对象的大小的综合超过幸存者空间的一半,大于等于改年龄的对象可以直接进入老年代)导致minorGC失去意义(该对象可能已经放到了老年代且该对象并不是一个长久对象),分带的思想失去意义.
-
如果伊甸园区分配的较少:伊甸园区空间满了之后触发minorGC,出现的频率比较高,影响用户进程,导致stw的时间总体变得多了
-
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails:输出详细的GC处理日志
- 打印GC简要信息: -XX:+PrintGC -verbose:gc
- -XX:HandlePromotionFailure:是否设置空间分配担保
- 详细介绍空间分配担保
- 详细介绍空间分配担保
10 堆是分配对象的唯一选择吗
堆空间是影响性能的瓶颈,让GC出现的频率低一些(主要是老年代的GC)
- 在<<深入理解java虚拟机>>中关于Java堆内存中有这样一段描述:随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么"绝对了".如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能优化成栈上分配.这样就无需在堆上分配内存,也无须进行垃圾回收了,这也是常见的堆外存储技术
- 基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象直接从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段
- 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
- 通过逃逸分析技术,JavaHotSpot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象动态作用域
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸.例如作为调用参数传递到其他地方中
- 为什么放到栈空间中:虚拟机栈每个线程一份,不会涉及到同步的问题,一个栈帧对应一个方法的调用,方法执行完直接弹出栈
- 逃逸分析概述
-
如何快速的判断是否发生了逃逸分析???---new的对象实体在外部是否被调用
-
- 参数设置
- 在JDK7中,HotSpot中默认就已经开启了逃逸分析
- 如果"-XX:+DoEscapeAnalysis"显示的开启逃逸分析
- 通过选项"-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果
- 开发中能使用局部变量的就不要在方法外定义
- 使用逃逸分析编译器可以对代码做如下优化
- 栈上分配:将堆分配转换为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会被逃逸,对象可能是栈分配的候选,而不是堆分配
- JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法,就可能被优化成栈上分配.分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量也被回收,这样就无需进行垃圾回收了
- 常见的栈上分配的场景:分别是给成员变量赋值,方法返回值,实例引用传递
-
-xx:-DoEscapeAnalysis:不开启逃逸分析(在堆空间中开辟空间)
-xx:+DoEscapeAnalysis:开启逃逸分析(在栈空间中开辟空间)执行时间比较短,不发生GC
- 栈上分配:将堆分配转换为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会被逃逸,对象可能是栈分配的候选,而不是堆分配
- 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
-
同步省略以及栈上分配是把字节码文件加载到内存中以后才进行的执行判断,编译之后在字节码文件中仍可以看到相关省略的操作(例如加锁)
-
- 分离对象或变量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中. 变形的栈上替换
11 总结