我们的堆内存分为:新生代,和年老代,
年轻代又分为:Eden区,幸存一区,幸存二区,
每一次访对象的时候,都是放入eden区域,和其中的一个幸存一区中,幸存二区是不放对象的,空闲的
当eden和幸存一区放满后,就会触发 minor gc,小型垃圾回收,把不再使用的对象,从内存中清空,给后面新创建的对象腾出来地方。
清理掉了不再使用的对象后,那么也会将存活下来的对象(还要继续使用的),放入幸存二区
这里出现一个问题,默认eden,幸存一区,幸存二区的内存占比是8:1:1,问题是,如果存活下来的对象是1.5,一个幸存区放不下,此时就可能通过JVM的担保机制,将多余的对象直接放入老年代
如果你的JVM内存不够大的话,可能导致频繁的年轻代内存满溢,频繁的进行minor gc,频繁的minor gc会导致短时间内,有些存活的对象,多次垃圾回收都没有回收掉,会导致这种短生命周期对象,年龄过大,垃圾回收次数过多,那么就会跑到老年代中去
老年代中可能会因为内存不足,可能会囤积一大堆,短生命周期,本来应该在年轻代中的,可能马上就要被回收掉的对象,此时,可能导致老年代频繁满溢,频繁进行full gc(全局、全面垃圾回收),full gc就会回收老年代的对象,full gc 由于算法设计,是针对的是,老年代中的对象数量很少,满溢进行full gc的频率应该很少,因此采取了不太复杂,但耗时和性能的算法,总的来说full gc很慢
full gc/minor gc,无论是快,还是慢,都会导致jvm的工作线程停止工作
在gc的时候,spark作业停止工作,等gc完毕才会再次工作
内存不足的时候,问题:
1. 频繁minor gc,也会导致频繁spark停止工作
2. 老年代囤积大量活跃对象(短生命周期的对象),导致频繁full gc时间很长,短则数十秒,长则数分钟,甚至数小时,可能导致spark长时间停止工作
3. 严重影响spark的速度
理想情况下,老年代都是放一些生命周期很长的对象,数量应该是很少的,比如数据库连接池
JVM调优的第一个点:
1. 降低cache操作的内存占比
spark中,堆内存又被划分成了俩块,一块儿是专门用来给RDD的cache、persist操作进行RDD数据缓存用的,另外一块,就是用来给spark算子函数的运行使用的,存放函数中自己创建的运行对象的。
默认情况下,给RDD cache操作的内存占比,是0.6,60%的内存都给了cache操作了,但是问题是,如果某些情况下,cache不是那么紧张,问题在于task算子函数中创建的对象过多,内存又不太大,导致了频繁的minor gc,导致spark频繁的停止工作,性能影响会很大
通过yarn去运行的话,那么就通过yarn的界面,去查看你的spark作业的运行统计,可以看到每个stage的运行情况,包括每个task的运行时间,GC时间,如果发现gc太频繁,时间太长,此时就可以适当调节这个比例。
降低cache操作的内存占比,大不了用persisit操作,将一部分数据写入磁盘,活序列化方式,配合Kryo序列化类,减少RDD缓存的内存占用,降低cache操作的内存占比;对应的,算子函数内存占比就提升了,这个时候,可能就可以减少minor gc的频率,同时减少full gc的频率,对性能有一定帮助
其实就是让task有更多的内存可以使用
cache的内存占比用以下的参数来调节
spark.storage.memoryFraction 默认值是0.6
这里我们适当的可以将其降低 0.6->0.5->0.4->0.2
.set("spark.storage.memoryFraction","0.5")