Jvm 组成部分
1. PC 寄存器 / 程序计数器
一块小的内存空间, 作用可以看做是当前线程所执行的字节码的行号指示器. 分支, 循环, 跳转, 异常处理, 线程恢复等基础功能都依赖计数器完成. 在任何一个时刻, 一个处理器只会执行一条线程中的指令, 因此, 为了线程切换后能恢复到正确的执行位置, 每条线程需要一个独立的程序计数器(私有内存).Java 方法的计数器记录正在执行的虚拟机字节码指令的地址, Native 方法的计数器为空. 计数器不存在 OutofMemoryError.
2. Jvm 栈
Jvm 栈也是线程私有的, 生命周期与线程相同. Jvm 栈描述的是 Java 方法执行的内存模型: 每个方法被执行的时候都会创建一个栈帧 (stackframe) 用于存储局部变量表, 操作栈, 动态链接, 方法出口等信息. 每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在 Jvm 栈从入栈到出栈的过程. 局部变量表存放了基本数据类型 (boolean,byte,char,short,int,float,long,double), 对象引用(指向对象地址的指针) 和 returnAddress(指向一条字节码指令的地址). 在方法运行期间不会改变局部变量表大小.
3. 本地方法栈
本地方法栈作用与 Jvm 栈作用类似, 区别不过是 Jvm 栈执行的是 Java 方法, 本地方法栈执行的是 Native 方法.
4. Jvm 堆
Jvm 堆是 Jvm 内存最大的一块, 被所有线程所共享. Jvm 堆在 Jvm 启动时创建, 此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存. Jvm 堆是垃圾收集器 (GC) 管理的主要区域. Jvm 堆可以处于物理上不连续的内存空间中, 只要逻辑上是连续的即可. 因为堆是 Jvm 中所有线程所共享的, 因此在其上进行对象内存分配均需要进行加锁, 这也导致了 new 对象的开销是比较大的. Sun Hotspot Jvm 为了提升对象内存分配的效率, 对于所创建的线程都会分配一块独立的空间 TLAB(Thread Local Allocation Buffer), 其大小由 Jvm 根据运行的情况计算而得, 在 TLAB 上分配对象不用加锁, 因此 Jvm 给线程分配对象的内存时会尽可能的在 TLAB 上分配, 如果对象过大, 则仍需在堆空间分配. TLAB 仅作用于 Eden Space.
5. 方法区
与 Jvm 堆一样, 方法区是各个线程共享的区域, 它用于存储已被虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据. 方法区也可以叫永生代, 但是 Hotspot 准备放弃永生代, 使用 Native Memory 来实现方法区. 相对而言, GC 在这个区域比较少的出现, 这个区域内回收目标主要是常量池和对类型的卸载.
6. 运行时常量池
运行时常量池是方法区的一部分, 这里存放的是类中的固定的常量信息, 方法和 Field 的引用信息.
Jvm 垃圾回收机制
触发 GC 的条件:
1)GC 在优先级最低的线程中进行, 一般在应用程序空闲, 即没有应用程序在运行时, 被调用.
2)例外: 当 Jvm 堆内存不足时, GC 会被调用. 当应用线程正在运行, 并且在运行过程中创建对象, 若这时内存不足, Jvm 会强制调用 GC. 若一次 GC 之后仍不能满足内存分配, Jvm 会再次进行两次 GC, 若仍不能满足, Jvm 报 OutofMemoryError,Java 应用停止.
GC 的 Generation 算法(分代收集算法)
分代收集算法是大部分 Jvm 的垃圾收集器采用的算法. 他的核心思想是根据对象存活的生命周期, 将内存划分为若干个不同的区域. 一般情况下将堆分为老年代和新生代, 方法区为永生代 (新版本将永生代废弃, 引入元空间的概念, 永生代使用 Jvm 内存儿元空间直接使用物理内存). 新生代分为 Eden 区和 Survivor 区(Survivor from 和 Survivor to), 大小比例默认为 8:1:1. 新产生的对象优先进入 Eden 区, 当 Eden 区满了之后再使用 Survivor from, 当 Survivor from 也满了之后, 就进行 Minor GC(新生代 GC), 将 Eden 和 Survivor from 中存活的对象使用 Copying 算法复制进入 Survivor to, 原来的 Survivor to 就成了新的 Survivor from,Copying 算法在复制之后清空 Eden 和 Survivor from, 原来的 Survivor from 就成了新的 Survivor to. 复制的时候, 如果 Survivor to 无法容纳全部存活的对象, 则根据老年代的分配担保, 将对象复制进入老年代, 如果老年代也无法容纳, 则进行 Full GC(老年代 GC 或者称为 Major GC). 老年代的特点就是每次垃圾收集时, 只有少量对象需要被回收, 而新生代的特点是每次都要回收大量的对象, 因此可以根据不同代的特点采取合理的收集算法. 程序中主动调用 System.gc() 强制执行的 GC 为 Full GC.
Jvm 对新生代采用 Copying 算法, 因为新生代每次 GC 都要回收大部分对象, 需要复制的操作次数较少;
老年代的特点是每次 GC 只回收少量对象, 一般使用 Mark-Compact 算法;
1)大对象直接进入老年代, Jvm 有个参数配置, 大于这个值直接进入老年代, 避免 Eden 和 Survivor 区之间发生大量的对象复制操作.
2)长期存活的对象进入老年代, Jvm 给每个对象定义一个对象年龄计数器, 如果对象在 Eden 出生, 经历一次 Minor GC 之后仍存活, 将其移入 Survivor 区并且年龄 + 1. 每经过一次 GC 年龄 + 1, 当年龄到达 15(Jvm 参数, 默认为 15), 移入老年代.
3)但是 Jvm 不是永远要求年龄必须达到最大年龄才可以晋升老年代, 如果 Survivor 空间中相同年龄 (如年龄 X) 的所有对象大小的总和大于 Survivor 的一半时, 年龄大于等于 X 的所有对象直接进入老年代.
简单的三个收集算法
1)标记清除算法:
分标记, 清除两个阶段. 首先标记所需要回收的对象, 在标记完成之后统一回收所有被标记的对象. 不足: 一个效率问题, 标记和清除的效率都不高; 一个空间问题, 标记清除之后产生了大量不连续的内存碎片, 碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不再触发一次 GC.
2)复制算法 Copying:
为了解决效率问题, 复制算法将可用的内存分为两部分, 每次只使用其中一块, 当一块内存用完了, 将还存活的对象复制到另一块上面, 然后再把刚刚用过的内存空间一次性清理掉, 这样解决了碎片问题.
3)标记整理算法 Mark-Compact:
复制算法在对象存活率较高时就会频繁进行复制操作, 效率将降低, 因此有了 Mark-Compact 算法. 标记过程和标记清除算法相同, 但是后续不是直接清除对象, 而是让所有存活的对象向同一侧移动, 然后直接清理边界以外的内存.
两种算法判定对象是否存活
1)引用计数算法: 给对象中添加一个引用计数器, 每当一个地方应用了对象, 计数器 + 1, 当引用失效, 计数器 - 1. 计数器为 0 表示该对象已死, 可回收. 但是这个方法很难解决两个对象循环相互引用的情况.
2)可大型分析算法: 通过一系列称为 "GC Roots" 的对象作为起点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链, 当一个对象到 GC Roots 没有任何引用链相连(即对象到 GC Roots 不可达), 则证明对象已死, 可回收. Java 中可以作为 GC Roots 的对象包括: Jvm 栈中引用的对象, 本地方法栈中 Native 方法引用的对象, 方法区静态属性引用的对象, 方法区常量引用的对象. 主流实现中, 都是通过可达性分析算法来判定对象是否存活.
来源: http://www.jianshu.com/p/c0b4a46aca35