深入理解java虚拟机
还未完成全部,后续更新,jvm实战内容
第2章 Java内存区域与内存溢出异常
Java与C++之间有一堵由内存动态分配和垃圾收集技术构成的围墙。虚拟机自动内存管理机制的帮助下,不需要去配对delete/free代码,不容易出现内存泄漏和以出问题。
2.1 运行数据区域
-
程序计数器 : 是一个小区域,为各个线程独有,可以看作是当前的字节码(Java方法)的行号指示器。 无OutOfMemoryError情况。
-
Java虚拟机栈 :为线程私有,并且生命周期与线程相同。用与存储局部变量表,操作数栈,动态链接,方法出口。 returnAddress类型(指向下一条字节码指令的地址,从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
- Java堆 : 是Java虚拟机管理内存中最大的一块,Java堆是被所有线程共享的一块区域。唯一目的存放对象实例,几乎所有对象实例在这里分配内存。Java是垃圾收集器管理的主要区域,很多时候称为GC堆。基本采用分代收集算法。当内存没有完成实例分配,并且堆无法扩展,将抛出OutOfMemoryError。
- 方法区 :所有线程共享。类信息、常量、静态变量、即时编译器后的代码。
- 本地方法栈 :本地方法栈区为虚拟机使用到的本地方法服务,会有StackOverflowError和OutOfMemoryError。
2.2对象的探秘
对象的创建
- 虚拟机遇到一条new指令,先去常量池中看能否定位到一个类的符号引用,并且检查这个引用代表的类是否已被加载、解析初始化过。如果类没有加载过,必须先执行类加载过程。
- 类加载检查通过后,虚拟机为新生对象分配内存。
- 设置对象头,包括类的元数据信息、对象的哈希码、对象的GC分代年龄。
- 以上工作都完成之后,从Java程序的视角,对象才刚刚开始创建-开始执行。
3 对象的内存布局
对象在内存中的存储可以分为三块区域:对象头、实例数据、和对齐填充。
对象头包括两部分信息,第一部分用与存储对象自身的运行时数据,如哈希码、GC分代年龄
锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分类型指针。
第3章 垃圾收集器与内存分配策略
3.1 概述
Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。而Java堆和方法区则不一样,一个接口的多个实现类可能需要的内存不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行时期才能知道创建那些对象,这部分内存都是动态创建和回收的。
3.2 对象已死吗
3.2.1 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为0对象就不可在引用。
缺点 :很难解决对象之间相互循环引用的问题。
3.2.2 可达性分析算法
基本思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。
3.2.3 生存还是死亡
即使是可达性分析算法中的不可达对象,也并非非死不可,它需要经历两次标记过程:如果对象在进行可达性分析后发现没有GC Roots相连接的引用链,那将会进行第一次标记并且进行一次筛查,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。
如果对象有必要执行finalize()方法,会将对象放置在一个叫做F-Queue的队列之中,并稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行它。
3.2.5 回收方法区
永久代(方法区)主要回收两部分内容:废弃常量和无用的类。回收无用的类需要同时满足下面三个条件:
- 所有类的实例都已经被回收。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.3 垃圾收集算法###
3.3.1 标记-清除算法
在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
3.3.2 标记 - 整理算法
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点 :不会产生内存碎片
缺点 :需要移动大量对象,处理效率比较低。
3.3.3 复制算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理
3.3.4 分代算法
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
新生代使用:复制算法
老年代使用:标记 - 清除 或者 标记 - 整理 算法
3.4 垃圾收集器
3.4.1 Serial收集器
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
3.4.2 Parnew收集器
它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
3.4.3 Parallel Scavenge 收集器
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
3.4.4 Serial Old 收集器
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
3.4.5 Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
3.4.6 CMS收集器
以获取最短回收停顿时间为目标的收集器。
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。
如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。