内容:
1.JVM GC的内存管理
2.频繁 GC 的原因
在JVM进行GC时,内存中存在新生代(年轻代)、老年代(年老代)、永久带。
在我们运行程序时,会创建很多对象。
1)从生命周期上看:
根据经验,绝大多数对象在使用过之后就不会在使用了。也就是说这些对象的使用的生命周期比较短。当然相应的,也有一少部分对象的生命周期会比较长。
2)从对象大小上看
在这些对象中会有很多小对象,和一些的大对象。
因此,JVM在进行GC时就要从两方面来考虑GC问题。
1)针对对象大小比较小且对象的生命周期比较短,会通过频繁GC把这些对象从内存中清除出去。
2)对于对象大小比较大或者对象的生命周期比较长,一般不会通过频繁GC的方式,而是将这些对象放到老年代。
新生代有Eden(伊甸园)、From(Survivor 1)、To(Survivor 2)区域。
Eden(伊甸园)主要用于存放新创建的对象,它会在第一次GC时,将对其内部的存活的对象放入到From或者To区域。但我们仍可以使用这些对象。
From与To区域都是用Survivor区域,主要用于GC时,在对象复制时,提供作用,以及承担新生代与老年代之间的缓冲带的作用。
就整个新生代来说,会进行频繁的GC。
而老年代,则存放经过几次(一定次数)的GC仍存在的对象或者是大对象以及生命周期比较长的对象。
GC的过程:
当对象创建后会进入Eden中、垃圾收集器进行垃圾回收时,会扫描Eden以及From区域。如果对象仍然存活,即在进行GC时,对象仍存活。此时,就会将该对象由Eden、From复制到To区域。如果从Eden或Form区域进行对象复制操作时,To区域已经满了,则会直接将对象放入到老年代(Old Generation)。而在对象复制后,To区域中存在的对象就是有效的对象,此时会将To区域与From区域交换,然后将无效对象清除,即将From区域(此时已经经过交换而成为To区域)与Eden清空。所以我们说Old Generation是经过几次GC后仍存在的对象,其实最小的次数有可能是0次,这是因为如果对象特别大,就会在其创建时,直接将其放入到老年代;也有可能是1次,例如:当进行第一次GC时,要将Eden中的对象放入到To区域,但是此时To区域已经满了,那么对象就会直接放入到老年代了。在GC时,JVM还会对From区域中的对象进行扫描,发现进过几次GC仍然存在的对象,也会放入老年代中。然后再进行清空操作。
从新生代GC的过程,我们可以看出年轻代的垃圾回收采用的是复制的方式进行。
其次,Eden中的对象复制到老年代之前会用个缓冲地带(Survivor区域),Survivor缓冲地带主要是方便对象复制而设计的。采用复制进行对象的回收的好处是,不会产生内存碎片,此处采用了空间换时间的方式来加快垃圾回收过程。(这里需要提供两倍的内存空间,Survivor1和Survivor2。)
对于老年代,其中放置的是进过几次GC仍然存在的对象,或者是大对象;老年代一般占用内存空间也较大,但是可以通过参数设置。相对而言,这部分不会进行频繁的GC(正常情况下,1h或者10h进行一次GC)。
1.若在服务器端发生Full GC(老年代发生GC)对性能影响相当大,可能会造成服务器不响应的现象。
2.Full GC:一般老年代和永久代发生GC,都为Full GC。
在老年代中,对象可以采用压缩方式存储也可以不采用压缩方式存储,主要看具体的实现。
对于永久代(PermanentGeneration)来说,其中存放的是类的定义,类的字节码,常量等,其中发生GC的时机或者条件:类的所有实例对象都被GC掉了,同时类的加载器也被回收掉了。
例如在代码中调用System#GC或者Runtime#GC方法。
在java程序调用相关框架时,框架内部调用了GC方法。
当heap大小设置比较小时,会引起频繁的GC,所以在类似于Spark这样对内存性能要求比较高的应用程序运行时,应可能给heap分配较大的内存,这样可以减少频繁的GC现象的发生。
当构建的对象实例化十分频繁并且释放该对象也较为频繁时,同样会产生频繁GC现象。
& Scala是一种面向函数式和面向对象相结合的编程语言。从面向函数式编程风格来说,创建的对象一般都是不可变的。这样就给内存的管理带来了极大的好处,避免由于对象频繁的改变而造成的GC。同时,不可变对象也可以在代码中被其他函数复用。
& 对用复用来说,是一把双刃剑。复用即通过缓存对对象进行使用。Cache级别的对象,若是其缓存命中很高时,此时应尽量复用这些对象。若cache命中率不高时,此时既占用内存,又会消耗IO、CPU等内存等资源。
& 一般来说,进行GC时,保证heap 50%的剩余空间,相对来说是比较合适的。并且需要统计缓存命中率的问题,其命中率越高越好。