Java堆内存与GC

2019-08-21  本文已影响0人  耐得千事烦

开设JVM系列以后,就一直打算写写GC这块的事情。总体上来说这块也算是比较偏理论,很多时候是看的时候理解了,一段时间以后又忘记了,所以打算不仅写写理论还会写上一些调优的相关例子,这样会加深印象。

我们由GC的角度来进行堆内存的区域划分:新生代老年代。而新生代又可以划分为Eden区SurvivorFrom区SurvivorTo区。而整个区域的比例应该是新生代比老年代为1:2,而Eden比SurvivorFrom比SurvivorTo为8:1:1

堆内存区域划分.png
说完堆内存的区域划分以后,我们再给出关于设置堆内存的相关参数大小的VM命令:
VM堆的相关参数 描述
-Xms 设置JVM启动时堆的初始化内存大小
-Xmx 设置JVM的堆最大可用内存大小
-Xmn 设置新生代的空间大小,剩下的为老年代的空间大小(-Xmn 是将NewSize与MaxNewSize设为一致)同下面两个参数-XX:NewSize=XXXm与-XX:MaxNewSize=XXXm
-XX:PermGen 设置永久代内存的初始化大小(1.8以后就没有永久代了,用元数据空间代替)
-XX:MaxPermGen 设置永久代的最大值(1.8以后就没有永久代了,用元数据空间代替)
-XX:MetaspaceSize 元数据空间初始化大小
-XX:MaxMetaspaceSize 元数据空间最大
-XX:SurvivorRatio 提供Eden区和survivor区的空间比例。比如,如果年轻代的大小为10m并且VM参数是-XX:SurvivorRatio=2,那么将会保留5m内存给Eden区和每个Survivor区分配2.5m内存。默认比例是8
-XX:NewRatio 提供年老代和年轻代的比例大小。默认值是2
-Xss Stack(栈)内存大小设置(每个线程都会产生一个栈。在相同物理内存下,减小这个值能生成更多的线程。如果这个值太小会影响方法调用的深度)
-XX:MaxTenuringThreshold 设置新生代代对象进入老年代的年龄(设置垃圾最大年龄。如果设置为0的话,则新生代对象不经过Survivor区,直接进入老年代。对于老年代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则新生代对象会在Survivor区进行多次复制,这样可以增加对象再新生代的存活时间,增加在新生代即被回收的概论)

通过上面的参数命令,大家可以尝试的进行相关JVM的内存设置,然后根据不同的设置来模拟各种OOM的情况。


1.对象被创建以后,进行内存分配的流程

首先会尝试是否能直接分配到栈上空间(这个跟JIT的逃逸分析有关),如果不能则再次尝试能否分配到TLAB上(本地线程分配缓冲区,存在与Eden区域),如果不能则对对象进行大小判断,如果是大对象(指的是占据了一个大量的连续内存空间的对象,如数组)则直接进入老年代如果不是大对象则直接进入新生代里的Eden区域

关于本地线程缓冲分配区域(TLAB)

由于堆内存是线程共享区域,在每次为对象进行内存空间分配的时候需要加锁操作,可以知道的是这个操作的开销是比较大的。所以针对这种情况,Sun Hotspot JVM为了提高对象内存分配效率,会为每个线程在堆内存区域开辟一个专属各个线程的缓存分配区域(Thread Local Allocation Buffer),在这个区域进行对象内存分配的时候是不用加锁的,所以效率都是很高的。但如果对象过大的话则仍然是直接使用堆空间分配。TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。

2. 几种内存分配策略

分配担保是老年代为新生代作担保。由于新生代中使用“复制”算法实现垃圾回收,老年代中使用“标记-清除”或“标记-整理”算法实现垃圾回收,只有使用“复制”算法的区域才需要分配担保,因此新生代需要分配担保,而老年代不需要分配担保。

3. 对象内存分配的俩种方法

为对象进行内存空间分配的任务,其实就是将一块确定大小的内存空间划走一片。

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表


什么是GC?字面意思解释就是垃圾回收。在我们Java里面,当对象创建好以后,我们是不需要关心对象的回收工作的,由JVM虚拟机会自动帮我们去回收这些对象,而JVM能这样做的原因就是因为这个GC垃圾回收机制。

1. 确定对象为垃圾的2种算法
public class ReferenceCountingGC {
    public Object instance;
    public ReferenceCountingGC(String name){}
}

public static void testGC(){

    ReferenceCountingGC a = new ReferenceCountingGC("objA");
    ReferenceCountingGC b = new ReferenceCountingGC("objB");

    a.instance = b;
    b.instance = a;

    a = null;
    b = null;
}

1. 定义2个对象
2. 相互引用
3. 置空各自的声明引用

循环引用.png

我们可以看到,最后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。

哪些是GC roots?
根据JVM规定只有虚拟机方法栈上和本地方法栈上引用的对象和方法区中类的静态属性引用的对象以及方法区中常量引用的对象

如何找到GC roots?
通过采用一个OopMap的数据结构来记录系统中存活的“GC Roots”,在类加载完成的时候,虚拟机就把对象内什么偏移量上是什么类型的数据计算出来保存在OopMap,通过OopMap就可以找到堆中的对象,这些对象就是GC Roots。而不需要一个一个的去判断某个内存位置的值是不是引用。这种方式也叫准确式GC

2. GC的分类

GC可以被划分为minorGCmajorGC。第一个是用来处理新生代区内的对象回收的GC,第二个是用来处理老年代和永久代(jdk8以后就没有永久代了)区内的对象回收的GC。至于Full GC网上众说纷坛我看了好几篇博客大概感觉fullGC应该可以理解为minorGC+majorGC。

堆内存中何时触发GC进行工作的?
minorGC 当Eden区的大小满了以后会触发minorGC来进行工作;
majorGC 当old区满了以后会触发majorGC来清理old区的对象,或者当老年代无法为新生代提供空间担保的时候则会触发majorGC来清理老年代对象为新生代腾出空间。

3. 垃圾回收算法

为什么堆内存要分区?
是为了更好的对堆内对象进行回收工作,在新生代中针对对象的朝生夕死特性,选择使用复制算法来进行GC工作,因为存活的对象少所以复制算法的效率很高。在老年代中采用的是标记压缩法来进行,因为存活的对象比较多如果采用复制算法则会导致效率很低。

4. 几种垃圾收集器

另外,可以通过-XX:+UseAdaptiveSizePolicy参数开启自适应调节策略,这样可以免去我们自己设置堆内存的一些细节参数,比如新生代内存大小,Eden与Survivor之间的比例等等。这个参数适合对内存手工优化存在困难的时候使用,他能监控系统当前的状态,动态的调整以达到最大的吞吐量

垃圾收集器是不能随意组合的,现在给出垃圾收集器互相的组合图:


垃圾收集器组合使用.png

至此,就把堆内存与GC相关的东西说完了,有点浅显,很多都是泛泛而谈。后面我会专开一篇文章来实战分析。

上一篇下一篇

猜你喜欢

热点阅读