共有五个,线程共享的有 堆(Heap)、方法去(Method Area), 线程私有的有 虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)和程序计数器。
以Java堆为例,探访对象的创建、内存布局和定位。
Java程序需要通过栈上的reference数据来操作堆上的 具体对象。 由于reference类型在Java虚拟机规范中只 规定了一个指向对象的引用,并没有定义这个引用应该 通过何种方式去定位、访问堆中的对象的具体 位置, 所以对象访问方式也是取决于虚拟机实现而定 的。 目前 主流的访问方式有使用句柄和直接指针两种。
垃圾收集(Garbage Collection,GC),最开始诞生于1960的Lisp.
程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭,这三个区域不考虑垃圾回收。Java堆和方法区是主要进行垃圾回收的区域。
举个 简单 的 例子, 请看 代码 清单 3- 1 中的 testGC() 方法: 对象 objA 和 objB 都有 字段 instance, 赋值 令 objA. instance= objB 及 objB. instance= objA, 除此之外, 这 两个 对象 再无 任何 引用, 实际上 这 两个 对象 已经 不可能 再被 访问, 但是 它们 因为 互相 引用 着 对方, 导致 它们 的 引用 计数 都不 为 0, 于是 引用 计数 算法 无法 通知 GC 收集 器 回收 它们.
可达性分析算法(Reachability Analysis)这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始 点, 从这些节点开始向下搜索,搜索所走过的路径称为引用链( Reference Chain),当一个对象到GC Roots没有任何引用链相连(用 图论的话来说,就是从GCRoots到这个对象不可达)时,则证明此对象是不可用的。
再谈引用(引用的定义及分类) 在 JDK 1. 2 以前,Java中的引用的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
在JDK 1. 2之后, Java对引用的概念进行了扩充,将引用分为强引用( Strong Reference)、软引用( Soft Reference)、 弱引用( Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
强引用:只要强引用还在永远不会被垃圾回收,定义方式通常为 A a = new A().
软引用:描述一些还有用但非必要的属性。在系统将要发生内存溢出 异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果 这次回收还没有足够的内存, 才会抛出内存溢出异常。
软引用:它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够, 都会回收掉只被弱 引用关联的对象。
虚引用:也成为幽灵引用或者幻影引用,最弱。为一个对象设置虚引用关联的唯一目的就是能在这个对象 被收集器回收时收到一个系统通知。
要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行 可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行 finalize() 方法。 当对象没覆盖finalize() 方法, 或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“ 没有必要 执行”。 如果这个对象被判定为有必要执行 finalize()方法, 那么 这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
finalize()方法是对象逃脱死亡命运的最后一次机会。
Java 虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集, 而且在方法区中进行垃圾收集的“ 性价比”一般比较低:在堆中,尤其是 在新生代中, 常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。 回收 废弃常量与回收Java堆中的对象非常类似。
如何判断一个类是否无用:
最基础的收集算法是“ 标记- 清除”( Mark- Sweep)算法,如同它的 名字一样, 算法分为“ 标记” 和“ 清除” 两个阶段: 首先标记出 所有 需要回收的对象, 在标记完成后统一回收所有被标记的对象。
不足:它的主要不足有两个: 一个是效率问题, 标记和清除两个过程 的效率都不高;另一个是空间 问题, 标记清除之后会产生大量 不连续 的内存碎片, 空间碎片太多可能会导致以后在程序运行过程中需要 分配较大对象时, 无法找到足够的连续内存而不得不提前触发另一次 垃圾收集。
为了解决效率问题, 一种称为“ 复制”( Copying) 的收集算法出现 了, 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是内存缩小为了原来的一半,未免太高了一点。
现在的商业虚拟机都采用这种收集算法来回收新生代, IBM公司的专门研究表明,新生代中的对象98% 是“ 朝 生 夕 死” 的, 所以并不需要按照 1: 1 的比例来划分 内存 空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当回收时,将Eden 和Survivor 中 还存活着的对象 一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的 Survivor 空间。HotSpot虚拟机默认的Eden和Survivor的大小为8:1
缺点:在对象存活率比较高时就要进行较多的复制操作,效率会变低。
(Mark-Compact)老年代一般采取该算法。
算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动, 然后直接 清理掉端边界以外的内存。
当前商业虚拟机的垃圾收集都采用“ 分 代 收集”( Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“ 标记— 清理” 或者“ 标记— 整理” 算法来进行回收。
在HotSpot的实现中, 是使用一组称OopMap的数据结构 来达到这个目的的, 在类加载完成的时候, HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来, 在JIT编译过程中,也会在特定的位置记录下栈和 寄存器 中哪些位置是引用。这样, GC在扫描时就可以直接得知 这些信息了。
在 OopMap的协助下,HotSpot可以快速且准确地完成 GC Roots 枚举,但一个很现实的问题随之而来:可能导致 引用关系变化,或者说OopMap内容变化的指令非常多, 如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间, 这样GC的空间成本将会变得很高。
实际上,HotSpot也的确没有为每条指令都生成 OopMap, 前面已经 提到,只是 在“ 特定 的 位置” 记录了 这些信息, 这些位置称为安全点( Safepoint),即 程序执行时并非在所有地方都能停顿下来开始 GC,只有 在到达安全点时才能暂停。
Safepoint机制保证了程序执行时, 在不太长的时间内 就会遇到可进入 GC的Safepoint。
安全区域(Safe-Region)是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe-Region看做是被扩展了的Safepoint。
这里讨论的收集器基于JDK1.7Update14之后的HotSpot虚拟机。
Serial 收集器是最基本、发展历史最悠久的收集器,曾经是虚拟机新生代收集的唯一选择。该收集器为单线程收集器,它在工作时会暂停其它线程。“Stop the world”指的就是这种情况。 优点:简单而高效( 与其他收集器的单线程 比), 对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集。
ParNew收集器其实就是Serial收集器的多线程版本, 除了使用多条线程进行垃圾收集之外,其余行为包括 Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样, 在实现上,这两种收集器也共用了相当多的代码。
它是许多运行在Server模式下的虚拟机中首选的新生代 收集器,其中有一个与性能无关但很重要的原因,原因 是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在 JDK 1. 5 时期, HotSpot 推 出了 一 款 在 强 交互 应用 中 几乎 可 认为 有 划时代 意义 的 垃圾 收集 器—— CMS 收集 器( Concurrent Mark Sweep, 本节 稍后 将 详细 介绍 这 款 收集 器), 这 款 收集 器 是 HotSpot 虚拟 机中 第一 款 真正 意义上 的 并发( Concurrent) 收集 器, 它 第一次 实现 了 让 垃圾 收集 线程 与 用户 线程( 基本上) 同时 工作, 用 前面 那个 例子 的 话来 说, 就是 做 到了 在 你的 妈妈 打扫 房间 的 时候 你 还能 一边 往 地上 扔 纸屑。 不幸 的 是, CMS 作为 老 年代 的 收集 器, 却 无法 与 JDK 1. 4. 0 中 已经 存在 的 新生代 收集 器 Parallel Scavenge 配合 工作[ 1], 所以 在 JDK 1. 5 中 使用 CMS 来 收集 老 年代 的 时候, 新生代 只能 选择 ParNew 或者 Serial 收集 器 中的 一个。
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同, CMS等收集器的关注点 是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。
Serial Old 是 Serial 收集 器 的 老年 代版本,它同样是一个单线程收集器, 使用“ 标记- 整理” 算法。 这个收集器 的 主要意义也是在于给Client 模式下的 虚拟机使用。如果在Serve 模式下,那么 它主要还有两大用途: 一种 用途 是在 JDK 1. 5 以及之前 的 版本中与 Parallel Scavenge收集器搭配使用[ 1], 另一种 用途就是作为CMS收集器的后备预案,在 并发收集发生Concurrent Mode Failure时 使用。
Parallel Old是Parallel Scavenge收集器 的老年代版本,使用多线程和“ 标记- 整理” 算法。这个收集器是在JDK 1.6中 才开始提供的。
CMS( Concurrent Mark Sweep)收集器 是 一种以获取最短回收停顿时间为目标的 收集器。 目前很大一部分的Java 应用 集中在互联网站或者 B/ S 系统的服务 端上,这类应用尤其重视服务的响应速度, 希望系统停顿时间最短, 以给用户带来 较好的体验。
从名字(包含" Mark Sweep") 上就可以 看出, CMS 收集器是基于“ 标记— 清除” 算法实现的,它的运作过程相对于前面 几种收集器来说更复杂一些, 整个过程 分为 4 个 步骤, 包括:
CMS是一款优秀的收集器,它的主要优点 在 名字上已经体现出来了: 并发收集、低 停顿,Sun公司的一些官方文档中也称之为 并发低停顿收集器( Concurrent Low Pause Collector)。
缺点:
G1( Garbage- First 收集器是当今收集 器技术发展的最前沿成果之一,早在JDK 1. 7 刚刚确立项目目标, Sun 公司给出 的 JDK 1. 7 RoadMap 里面,它就被视为 JDK 1. 7 中HotSpot 虚拟机的一个重要进化 特征。
G1 是一 款 面向 服务 端 应用 的 垃圾 收集 器。 HotSpot 开发 团队 赋予 它的 使命 是( 在 比较 长期 的) 未来 可以 替换 掉 JDK 1. 5 中 发布 的 CMS 收集 器。 与其 他 GC 收集 器 相比, G1 具备 如下 特点。
虚拟机提供了- XX:+ PrintGCDetails 这个 收集器日志参数, 告诉虚拟机在发生垃圾 收集行为时打印内存回收日志, 并且在 进程退出的时候输出当前的内存各区域 分配情况。 在实际应用中, 内存回收 日志一般是打印到文件后通过日志工具 进行分析, 不过本实验的日志并不 多,直接阅读就能看得很清楚。
新生代GC( Minor GC): 指发生在 新生代的垃圾收集动作, 因为Java对象 大多都具备朝生夕灭的 特性, 所以 Minor GC 非常 频繁,一般回收速度也比较快。
老年代GC( Major GC/ Full GC): 指发生在老年代的 GC,出现了Major GC, 经常会伴随至少一次的Minor GC( 但非 绝对的,在 Parallel Scavenge 收集器 的收集策略里就有直接进行 Major GC 的 策略 选择 过程)。** Major GC的速度 一般 会 比 Minor GC慢10倍以上。**
所谓的大对象是指, 需要大量连续内存 空间的 Java 对象, 最典型的大对象 就是 那种很长的字符串以及数组。 大对象对 虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空 间时就提前触发垃圾收集以获取足够的 连续 空间 来“ 安置” 它们。
如果对象在Eden 出生并经过第一次 Minor GC 后仍然存活, 并且能被 Survivor 容纳的 话,将被移动到 Survivor 空间中, 并且 对象年龄设为1。 对象在 Survivor 区 中 每“ 熬过” 一次 Minor GC,年龄就增加 1 岁, 当它的年龄增加到一定程度( 默认 为 15 岁), 就将会被晋升到老年代 中。 对象晋升老年代的年龄阈值, 可以通过参数- XX: MaxTenuringThreshold 设置。
如果在 Survivor 空间中相同年龄所有对象大小的总和大于Survivor 空间的一半,年龄 大于或等于该年龄的对象就可以直接进入老年代, 无须等到 MaxTenuringThreshold 中 要求的年龄。
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次 Minor GC是有风险的; 如果小于,或者HandlePromotionFailure 设置不允许冒险, 那这时也要改为进行一次Full GC。
给一个系统定位问题的时候,知识、 经验是关键基础,数据是依据,工具 是运用知识处理数据的手段。 这里说的数据包括: 运行日志、 异常堆栈、 GC 日志、 线程 快照( threaddump/ javacore 文件)、 堆 转储快照( heapdump/ hprof 文件) 等。 经常使用适当的虚拟机监控和 分析的工具可以加快我们分析数据、 定位解决问题的速度。
《深入理解Java虚拟机:JVM高级属性与最佳实践》读书笔记(更新中)
原文:http://www.cnblogs.com/waliwaliwa/p/7270868.html