JVM内存模型
内存各区域作用
标记清除算法
复制算法
标记整理
可达性算法
可达性算法的GC roots
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区常量池中的常量
- 方法区静态成员引用的对象
G1
堆的划分
默认比例
新生代:老年代 = 1:2
新生代中分eden区和两个survivor,比例8:1:1
新生代老年代的比例思考
web应用中的对象基本可以分成这三种。
( a) 有一部分对象几乎一直活着。这些可能是常用数据的cache之类的
( b) 有一部分对象创建出来没多久之后就没用了。这些很可能会响应一个请求时创建出来的临时对象
( c) 最后可能还有一些中间的对象,创建出来之后不会马上就死,但也不会一直活着。
新生代快满的时候会触发young GC/minor GC,老年代快满会出发full GC,调整分区比例就是分配大小,越小的内存,其对应的GC频率就会高。
- 新生代比例过大,老年代比例小
比例太大的话,会导致minor GC频率太低,一些我们希望进入老年代的对象一直不能进入老年代。新生代太大,那么每次minorGC的时长太久,并且因为老年代内存小,会经常触发fullGC(会清理新生代和老年代),这样就会导致频繁的Stop-The-Word,且时间很长。 - 老年代比例过大,新生代比例小
新生代比例小,minorGC频率高,虽然每次的时间会很短,但可能有些我们不想它进老年代的也进了老年代。还有可能有些临时对象刚创建还没使用就被回收了。
survivor的思考
- 为什么要有survivor
我们看看如果没有 Survivor 空间的话,垃圾收集将会怎样进行:一遍新生代 gc 过后,不管三七二十一,活着的对象全部进入老年代,即便它在接下来的几次 gc 过程中极有可能被回收掉。这样的话老年代很快被填满, Full GC 的频率大大增加。我们知道,老年代一般都会被规划成比新生代大很多,对它进行垃圾收集会消耗比较长的时间;如果收集的频率又很快的话,那就更糟糕了。基于这种考虑,虚拟机引进了“幸存区”的概念:如果对象在某次新生代 gc 之后任然存活,让它暂时进入幸存区;以后每熬过一次 gc ,让对象的年龄+1,直到其年龄达到某个设定的值(比如15岁), JVM 认为它很有可能是个“老不死的”对象,再呆在幸存区没有必要(而且老是在两个幸存区之间反复地复制也需要消耗资源),才会把它转移到老年代。
总之,设置 Survivor 空间的目的是让那些中等寿命的对象尽量在 Minor GC 时被干掉,最终在总体上减少虚拟机的垃圾收集过程对用户程序的影响。
- 为什么要有两个survivor
我们来看看在只有一个 Survivor 空间的情况下,这个 8:1 会有什么问题。此处为了方便说明,我们假设新生代一共为 9 MB 。对象优先在 Eden 区分配,当 Eden 空间满 8 MB 时,触发第一次 Minor GC 。比如说有 0.5 MB 的对象存活,那这 0.5 MB 的对象将由 Eden 区向 Survivor 区复制。这次 Minor GC 过后, Eden 区被清理干净, Survivor 区被占用了 0.5 MB ,还剩 0.5 MB 。到这里一切都很美好,但问题马上就来了:从现在开始所有对象将会在这剩下的 0.5 MB 的空间上被分配,很快就会发现空间不足,于是只好触发下一次 Minor GC 。可以看出在这种情况下,当 Survivor 空间作为对象“出生地”的时候,很容易触发 Minor GC ,这种 8:1 的不对称分配不但没能在总体上降低 Minor GC 的频率,还会把 gc 的时间间隔搞得很不平均。把 Eden : Survivor 设成 1 : 1 也一样,每当对象总大小满 5 MB 的时候都必须触发一次 Minor GC ,唯一的变化是 gc 的时间间隔相对平均了。
设置两个survivor可以保证永远有一个是空的,每次Minor GC将一个survivor和Eden区的存活对象复制到另一个survivor,再清空这个survivor和Eden,每循环一次记一次数。
什么时候进行MinorGC和FullGC
触发Minor GC
1 Eden区满了会触发Minor GC
触发FullGC
1 调用System.gc,建议执行full GC,但不一定马上执行
2 老年代空间不足
3 方法区空间不足
4 通过Minor GC进入老年代的对象内存大于老年代剩余内存
5 堆中分配很大的对象(新生代存不下的会直接进入老年代),而老年代没有足够的空间
创建对象的过程
堆栈的数据结构
- 栈:具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。
- 堆:经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意的。
为什么要划分堆和栈
- 栈用来代表逻辑,堆用来代表数据。这样一来逻辑就一目了然了。
- 堆里面的数据是线程共享,栈是私有的,划分开可以节省空间,不需要创建相同的共享堆数据。
判断对象是否回收的流程
类加载过程