堆
2020-06-02 13:53:27 0 举报
AI智能生成
JVM堆
作者其他创作
大纲/内容
(1) 堆的核心概念
基本概念和理解
(1)一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
(2)Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间
堆内存的大小是可以调节的
(3)《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
(4)所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)
(5)《Java虚拟机规范》中对Java堆的描述是:所有的实例对象以及数组都应当在运行时分配在堆上。(The heap is run-time data area from which memoy for all class instances and arrays is allocated)
“几乎”所有的对象实例都在这里分配内存。——从实际使用角度看的
(6)数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
(7)在方法结束后,堆中的对象不会马上被移除掉,仅仅在垃圾收集的时候才会被移除
(8)堆,是GC(Garbge Collection,垃圾收集器) 执行垃圾回收的重点
图示
内存细分
现代垃圾收集器大部分都基于分代收集理论设计,对空间细分为:
约定:新生区 <=> 新生代 <=> 年轻代 养老区 <=> 老年区 <=> 老年代 永久代 <=>永久代
堆空间内部结构
(2) 设置堆内存大小和OOM
堆空间大小的设置
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项“-Xmx”和“-Xms”来进行设置
“-Xms”用于表示堆区的起始内存,等价于-XX:InitialHeapSize
“-Xmx”则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常
通常会将-xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下,初始化内存:物理电脑内存大小/64 最大内存大小:物理内存/4
OutOfMemory举例
(3)年轻代与老年代
JVM对象划分
存储在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
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
开发人员可以通过选项“-XX:SurvivorRatio”调整整个空间比例。比如-XX:SurvivorRatio=8
几乎所有的Java对象都是在Eden区被new出来的
绝大多数的Java对象的销毁都在新生代进行了
IBM公司的专门研究表明,新生代中80%的对象都是”朝生夕死“的
可以使用选项”-Xmn“设置新生代最大内存大小
这个参数一般使用默认值
参数列表
-XX:NewRatio:设置新生代与老年代的比例。默认值是2
-XX:SurvivorRatio:设置新生代中Eden区与Survivor区的比例
-XX:-UseAdaptiveSizePolicy:关闭自适应的内存分配策略
-Xmn:设置新生代的空间大小
(4)图解对象分配过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生新的内存碎片
(1)new的对象先放伊甸园区,此区有大小限制
(2)当伊甸园区的空间填满时,程序又需要创建对象,JVM的垃圾回收将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象引用的对象进行销毁。再加载新的对象放到伊甸园区
(3)然后将伊甸园中剩余对象移动到幸存者0区
(4)如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区后,如果没有回收,就会放到幸存者1区
(5)如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
(6)啥时候能去养老区呢?在每一次垃圾回收时序号加一,如果在每一次垃圾回收中都没有被清理达到十五次时就被放入老年区了(参数可以设置)
可以设置参数:-XX:MaxTenuringThreshold=<N>进行设置
(7)在养老区,相对悠闲。当养老区内存不足时,再触发GC;Major GC,进行养老区的内存清理
(8)若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
图解步骤
总结
针对幸存者s0,s1区的总结;复制之后有交换,谁空谁是to
关于垃圾回收;频繁的在新生区收集,很少在养老区收集,几乎不在永久区
代码演示
(5)Minor GC,Major GC,Full GC
概述
JVM在进行GC时,并非每次都对上面三个内存(新生代,老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代
针对HotSpot VM实现,它里面的GC按照回收区域又分为两大种类型;一种是部分收集(Partial GC),一种是整堆收集(Full GC)
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
新生代收集(Minor GC/Younf GC);只是新生代(Eden\S0,S1)的垃圾收集
老年代收集(Major GC / old GC);只是老年代的垃圾收集
目前,只有CMS GC会又单独收集老年代的行为
注意,很多时候Major GC会和 Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
目前,只有G1 GC 会有这种行为
整堆回收(Full GC):收集整个Java堆和方法区的垃圾收集
最简单的分代式GC策略的触发条件
年轻代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慢10倍以上,STW的时间更长
如果Major GC后,内存还不足,就报OOM了
Full GC的触发机制
调用System.gc()时,系统建以执行Full GC,但是不必要执行
老年代空间不足
方法区空间不足
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
由Eden区,survivor,space0(From Space)区向survior space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,老年代的可用内存小于该对象大小
full gc是开发或调优中尽量要避免的,这样暂停时间会短一些
(6)堆空间分代思想
经研究,不同对象的生命周期不同。70%-99%的对象是临时对象
新生代:有Eden,两块大小相同的Survivor(又称from/to,s0/s1)构成,to总为空。
老年代:存放新生代中历经多次GC仍然存活的对象
图示
其实可以完全不分代,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一起,就如同把一个学校的人都关在一个教室里。GC的时候要找到哪些对象没有这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的。如果分代的话,把新创建的对象放到某一个地方,当GC的时候先把这块存储“朝生夕死”的对象区域进行回收,这样就会腾出很大的空间出来
图示
(7)内存分配策略
基本概念
如果对象在Enen出生并经过第一次MinorGC后依然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor区中每熬过一次MinorGC,年龄·就增加1岁,当它的年龄增加到一定程度(默认15岁,其实每个JVM,每个GC都有所不同)时,就会被晋升到老年代中
对老年代的晋升的年龄阈值,可以通过选择 -XX:MaxTenuringThreshold来设置
不同年龄段分配原则
优先分配到Eden
大对象直接分配到老年代
尽量避免程序中出现过大的对象
长期存活的对象分配到老年代
动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
(8)为对象分配内存:TLAB
为什么有TLAB?
堆区是线程共享区域,任何线程都可以访问到堆区中共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下堆区中划分内存空间是线程不安全的
为了避免多个线程操作统一地址,需要使用加锁等机制,进而影响分配速度
什么是TLAB?
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
所有OpenJDK衍生出来的JVM都提供了TLAB的设计
对象分配过程TLAB(图示)
TLAB的再说明
尽管不是所有的对象实例都能够再TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间
默认情况下,TLAB的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占Eden空间的百分比大小
一旦对象再TLAB空间分配内存失败时,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接再Eden空间中分配内存
对象分配过程
(9)小节堆内存的参数设置
相关参数设置
-XX:+PrintFlagsInitial:查看所有的参数的默认值
-XX:+PrintFlagsFinal:查看所有参数的最终值(可能会存在修改,不再是初始值)
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代再堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreashold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输入详细的GC处理日志
打印GC简要信息
-XX:+PrintGC
-verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保
再次概述
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
如果小于,则此次Minor GC是安全的
如果大于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象平均大小
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的
如果小于,则改为进行一次Full GC
如果HandlePromotionFailure=false,则改为进行一次Full GC
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了 HandlePromotionFalure参数,但是在代码中已经不会使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC
(10)堆是分配对象的唯一选择吗
堆是对象存储的唯一选择吗?
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象分配到栈上也逐渐变得不那么“绝对”了
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收了。这也是最常见的堆外存储技术。 此外,前面提到的基于OpenJDK深度定制TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
逃逸分析概述
如何将堆上的对象分配到栈,需要使用逃逸分析手段
这是一种可以有效减少Java程序种同步负载和内存堆分配压力的跨函数全局数据流分析算法
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
逃逸分析的基本行为就是分析对象动态作用域:
当一个对象在方法种被定义后,对象只在内部使用,则认为没有发生逃逸。
当一个对象在方法种被定义后,他被外部房啊所引用,怎认为发生逃逸。例如作为调用参数传递到其他地方去
在JDK 6u23版本之后,HotSpot种默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
选项“-XX:+DoEscapeAnalysis”显式开启逃逸分析
通过选项“-XX:+PrintEscapeAnalysis”查看逃狱分析的筛选结果
结论
开发中能使用局部变量的,就不在方法外定义
代码分析
逃逸分析:代码优化
使用逃逸分析,编译器可以对代码做如下优化
(一)栈上分配。将堆分配转化为栈分配。如果一个对象在子程序种被分配,要使指向该对象的指针永远不会逃逸,对象可能式栈上分配的候选,而不是堆分配
(二)同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
代码示例
(三)分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量
相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量
再JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来替代,这个过程就是标量替换。
代码示例
上述代码在主函数进行1亿次alloc。调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB.如果堆空间小于这个值,就必然发生GC。使用下列参数运行上述代码:
-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
参数-server:启动Server模式,因为在Server模式下,才可以启用逃逸分析
参数 -XX:+DoEscapeAnalysis:启用逃逸分析
参数-Xmx10m:指定了堆空间最大为10MB
参数-XX:+PrintGC:将打印GC日志
参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配
标量替换参数设置
参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束了,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了
常见的栈上分配的场景
在逃逸分析中,已经说明了。分别是给成员变量赋值,方法返回值,实例引用传递
线程同步的代价是很高的,同步的后果是降低并发性和性能
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步代码块使用的锁对象是否只能够被一个线程访问没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能够提高并发性和性能。这个取消的同步过程就叫同步省略,也叫锁消除
0 条评论
下一页