Java 内存模型
Java 内存模型分为 5 个部分,程序计数器,虚拟机栈,本地方法栈,堆,方法区
-
程序计数器:线程私有,每个线程都有自己的程序计数器,指向正在执行的字节码地址。这个内存区也是 Java 内存模型中唯一一个不会发生内存溢出的区域。
-
虚拟机栈:线程私有,方法的执行都会涉及到虚拟机栈的入栈出栈操作。包含局部变量表、操作数栈、动态连接栈,局部变量表保存方法参数和方法内部变量,对于实例方法来说,局部变量表还保存着当前类的引用 this. 动态连接表,是用于指向常量池的。
-
本地方法栈:线程私有,保存 native 方法的信息。
-
堆:线程共享,保存 new 出的对象实例和数组,该区域也是垃圾回收的主要对象。
-
方法区:线程共享,保存的是类信息,常量,静态变量等数据,该区域也会进行垃圾回收,属于长久代的内存区域。运行时常量池就是方法区的一部分,保存编译期间的常量,不过并不只是编译器可以保存,在运行期也可以保存,比如 String 的 intern() 方法,如果常量池不存在该字符串常量,则将该字符串常量放入常量池。
四种引用
-
强引用,就是我们平时使用的最常见直接 new 处的对象。强引用的对象只有在不可达时才会被回收。
-
软引用,是在内存不足时才会被回收,内存够用的时候不会被回收
-
弱引用,是每次发生 GC 时就会被回收,不管内存是否够用
-
虚引用,随时都有可能会被回收,主要用于跟踪对象被垃圾回收时的活动
怎样判断垃圾需要回收
1. 引用计数
引用计数是给每个对象一个标记,每增加一个引用,标记就加 1, 在垃圾回收时标记是 0 则对其进行回收。
这样的算法有一个缺点就是假如有下面这样的代码
String a = new String("123");
String b = a + "ab";
这之后就没有再引用 a, b 的地方了,但是由于 a 的标记值为 1, 所以一直不能被回收。
2. 可达性分析
只要是没有被 GC Root 引用的对象就可以回收,可作为 GC Root 的引用为:虚拟机栈中引用的对象,方法区中常量池的对象,方法区中类静态属性对象,本地方法栈 JNI 引用的对象。
3. 方法区回收的判断
常量:没有再被引用
类:
- 该类的所有的实例都已经被回收
- 该类的类加载器已经被回收
- 没有任何对该类的引用,也没有对该类的反射
垃圾回收算法
1. 标记清除算法
标记清除分为标记阶段和清除阶段,标记可回收的对象,在清除阶段对其进行回收,这样的算法缺点就是会产生空间碎片较多,若给大对象分配内存空间时,就有可能提前触发 GC.
2. 复制算法
复制算法把内存空间分为一块 Eden 区域和两块 Suvivor 区域,其比例是 8:1:1, 有对象生成时优先分配到 Eden 区域,第一次发生 GC, 把 Eden 区域的存活的对象复制到 S1, 然后清除掉 Eden 区域所有的对象,再有新对象生成还是优先分配到 Eden 区域,第二次 GC 时,把 Eden 区域和 S1 区域存活的对象复制到 S2, 然后清除掉 Eden 区域和 S1 区域的对象。之后再发生 GC S1 和 S2 区域的就会来回倒腾。
复制算法适用于新生代的对象,因为新生代对象存活的时间都较短。
3. 标记整理算法
不直接对不可用对象进行回收,先把可用对象整理到一边,再清理边界外的不可用对象。
4. 分代收集
针对新生代和老年代使用不同的回收算法。
垃圾回收器
-
Serial : 新生代(复制算法),单线程,需暂停用户线程
-
Serial Old : 老年代(标记-整理),单线程,需暂停用户线程
-
Parallel Savenge : 新生代(复制算法),多线程,需暂停用户线程。吞吐量优先,两个参数:MaxGCPauseMills(GC停顿时间),GCTimeRation(GC吞吐量倒数,自适应调节策略)
-
Parallel Old : 老年代(标记-整理),多线程,需暂停用户线程,吞吐量优先。
-
ParNew : 新生代(复制算法),多线程,需暂停用户线程。Serial 的多线程版本。
-
CMS : 以获取最短回收时间为目标,老年代(标记-清除)
过程:
初始标记,需 stop the world, 标记直接与 GC Root 关联的对象;
并发标记,可与用户线程并发执行,标记所有被 GC Root 引用的对象;
重新标记,需 stop the world, 修改用户线程执行过程中已被标记的对象重新被引用的;
并发清除,可与用户线程并发执行,清除被标记的对象。
由于回收算法使用的是标记-清除,所以就会导致有碎片产生,可通过设置 UseCMSCompactAtFullCollection(Full GC 时是否开启碎片整理,默认开启), CMSFullGCsBeforeCompaction(设置执行多少次 GC 不压缩后,执行一次碎片整理,默认是 0, 每次都整理碎片)
由于并发,无法处理浮动垃圾,也就是在 GC 进行过程中用户线程还在执行产生的垃圾,GC 运行中需要预留一部分空间供用户线程使用,如果预留空间不足,则会触发 “Concurrent Mode Failure”, 临时启动 Serial Old 来重新进行老年代垃圾收集。
由于并发,会导致用户线程变慢,于是出现了“增量式并发收集器”,GC 与用户线程交替执行,效果很一般,已不提倡使用。
- G1 收集器
G1 收集器实现了分代收集,采用并行和并发,充分利用了多 CPU、多核,目标是低停顿,还建立了可预测的停顿时间模型,可指定在 M 时间内,GC 的时间小于 N.
JVM 内存分配策略
-
对象优先在新生代 Eden 区分配,当 Eden 区没有内存可分配时,触发一次 Minor GC.
-
大对象直接进入老年代,其中有一个参数 PretenureSizeThreShord, 大于改值的对象直接进入老年代。
-
长期存活对象进入老年代,Eden 区的对象,经过一个 Minor GC 后依然存活的进入 Survivor 区,且年龄设为 1, 此后每经过一次 Minor GC, 年龄加 1, 当年龄大于 MaxTenuringThreShold 时进入老年代。
-
动态对象年龄判定:当大于等于某一年龄的对象大小总和大于所有空间的一半时,大于等于某一年龄的对象进入老年代。
-
空间分配担保:当 Survivor 上没有足够的空间存 Eden 区经过 Minor GC 存活下来的对象时,这些对象将直接通过空间分配担保进入老年代。