深入理解Java虚拟机第二版
第一章 Java
Java发展史虚拟机 实战编译自己的虚拟机
第二章 Java内存区域与内存溢出异常
1.运行时数据区域
包括一下几个:
程序计数器:一块较小的内存空间,它可以看作当前线程所执行字节码的行号指示器。分支跳转循环异常都是依赖这个计数器完成。每个线程都需要一个独立的程序计数器,各线程之间互不影响,独立存储,为线程私有内存。
Java虚拟机栈:线程私有的,生命周期与线程相同。描述为Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈:Native Method Stack 与虚拟机栈发挥的作用非常相似,他们之间的区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用的Native方法服务。
Java堆:对于大多数应用来说Java虚拟机所管理的内存最大的一块。Java堆被所有线程所共享的内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有对象实例以及数组都在堆上分配内存。
Java堆是垃圾回收主要区域,具体回收以后介绍。Java堆可以在物理不连续的内存空间,只要逻辑上联系即可,如果堆中没有内存分配实例,并且堆也无法拓展时,将会抛出OutOfMemoryError
方法区:与Java堆一样是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 错称永久区
方法区无法满足内存分配需求时,将会抛出OutOfMemoryError
运行时常量池:是方法区的一部分。Class文件除了有类版本字段方法等描述信息外还有常量池。用于存放编译期生成的各种字面量和符号引用,这部分信息将在类加载后进入方法区的运行时常量池存放。
直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域,它可以使用本地函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuff对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免在堆中和Native堆中来回复制数据。内存区域总和大于物理内存时,也会报OutOfMemoryError异常
2.对象的创建
虚拟机遇到new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有则必须先执行类的加载过程。
类加载检查通过后,接下来虚拟机将为新生对象分配内存。 分配内存等同于把一块确定大小的内存从Java堆中划分出来。Java堆是完整的话,用过的内存放在一边,空闲内存放在一边,中间放置一个指针作为分界点指示器,这种分配方式成为指针碰撞。如果Java堆内存并不完整,虚拟机必须维护一个列表,上面记录哪块内存是可用的,分配内存的时候在列表中找到一块足够大的空间划分给对象,这种分配方式叫做空闲列表。分配内存也有可能出现并发问题,虚拟机采用CAS配上失败重试方式保证更新操作原子性;另一种方式是把内存分配动作按照线程划分在不同空间进行,即每个线程在Java堆中预先分配一小块内存,称为本地分配缓冲。只有本地线程分配缓冲用完了才需要同步锁定。
内存分配完了,虚拟机需要把分配的内存空间都初始化为零值。
接下来虚拟机要对对象进行必要的设置,例如对象是那个类的实例,对象hash码,GC分代信息灯光,这些信息存放在对象的对象头中。
上面的工作都做完之后,从虚拟机的视角来看,一个新的对象就已经产生了。
3.对象的内存布局
在HotSpot虚拟机中,对象在内存中的存储布局可分为三块区域:对象头、实例数据和对其填充。
对象头:包含两部分信息,第一部分用于存储对象自身的运行时数据。另一部分是类型指针,即对象指向他类元数据的指针。
4.对象的访问定位
目前主流访问方式有两种使用句柄和直接指针
使用句柄访问的话,Java堆中将会划分一块内存来作为句柄池,reference中存储的就是对象句柄地址,而句柄中包含对象实例数据与类型数据各自具体地址信息
使用直接指针访问,Java堆对象的布局中必须考虑如何返回放置访问类型数据的相关信息,而reference中存储的就是对象地址
两种方式各有优势
使用句柄最大的好吃就是reference中存储的句柄地址是稳定的,当对象被移动之后改变句柄中实例数据指针,而reference不需要改变
使用直接指针访问最大的好处就是速度更快,他节省了一次指针定位时间。目前Sun HotSpot采用的是直接指针方式。
第三章 垃圾收集器与内存分配策略
5.对象已死
引用计数法:给对象添加一个引用计数器,每当有地方引用他,计数器就加一;当引用失效时引用就减一;任何时刻计数器为0这个对象就不能被使用了。实现简单,判定效率高。相互循环引用问题,objA.instance = objB/objB.instance=objA;
他们的引用计数都不为0但是GC无法回收它们。
可达性分析算法:基本思想是通过一系列称为GC roots的对象作为起点,从这些起点开始向下搜索,搜索走过的路径称为引用链,当一个对象没有引用链相连时,则证明此对象是不可用的。
6.引用
Java将引用分为四种
强引用:程序代码中普遍存在,只要强引用存在就不会被回收。Object obj=new Object
软引用:用来描述一些还有用但非必需的对象。系统会在发生内存溢出异常之前将这些对象回收。如果二次回收还是不够才会抛出内存溢出异常。Soft Reference来实现软引用
弱引用:也是用来描述非必需的对象,但他比软引用更弱一下,只能存活到下一次GC之前。WeakReference类来实现弱引用
虚引用:也称幽灵引用,它是最弱的引用关系。一个对象是否存在虚引用完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的就是在这个对象被GC的时候收到一个系统通知。PhantomReference类来实现虚引用
7.标记清除算法
MarkSweep算法:首先标记所有需要回收的对象,在标记完成统一回收被标记对象
不如:效率问题标记清除效率不高;产生内存碎片
8.标记复制算法
为了解决效率问题,他将一块内存划分为大小相等的两块,每次使用其中一块。当其中一块使用完就将还活着的对象复制到另一块,再把已使用的一次清理掉
代价:内存缩小为原来一半
9.标记整理算法
标记过程相同,后续不是直接对可回收对象进行清理,而是所有存活对象向一端移动,然后直接清理掉端边界以外的内存。适用于老年代,对象存活多,没有额外空间担保
10.分代收集算法
98的对象都是朝生夕死,HotSpot虚拟机默认Eden和Servivor大小比例为8:1,也就是每次新生代可用内存为90%,当Servivor空间不够用时,需要依赖其他内存进行担保分配。没有足够空间直接进入老年代
11.HotSpot算法实现
枚举根节点可达性分析需要逐个检查里面的引用,必然会消耗很多时间,另外可达性对执行时间的敏感还体现在GC停顿上,,因为这项工作必须在一个能确保一致性的快照中进行。
在目前主流的虚拟机中用的都是准确式GC,当系统停顿下来之后并不需要一个不漏检查引用,虚拟机应当有办法知道哪些地方存放对象的引用。使用一组称为OopMap的数据结构。
安全点:如果为每一条指令都生成OopMap那将需要大量额外空间,因此只是在特定位置记录这些位置,这些位置称为安全点。即程序执行只有在安全点才能暂停。
主动式中断思想是当GC需要中断时,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行主动去轮询这个标志,发现中断标志为真就自己中断挂起。
12.Serial收集器
Serial收集器是最基本发展最悠久的收集器。这个收集器是单线程收集器,但它的单线程只会使用一个CPU或一条线程去完成垃圾收集器。他在进行垃圾回收时必须暂停其他所有线程直到他收集结束。从jdk1.3到最新的jdk1.7HotSpot虚拟机开发团队为消除或减少工作线程因内存回收的停顿一直进行努力。但实际上,到目前为止他依然是虚拟机在client模式下的默认新生代收集器。
13.ParNew收集器
就是Serial收集器的多线程版本,其他行为完全一样。尽管他与Serial收集器相比电话并没有太多创新,但他却是许多运行在Server模式下的虚拟机中首选新生代收集器,其中有一个与性能无关的原因,目前只有它能与CMS收集器配合工作。
14.Parallel Scavenge收集器
Parallel Scavenge收集器是Java虚拟机中垃圾收集器的一种。
又称为吞吐量优先收集器,和ParNew收集器类似,是一个新生代收集器。使用复制算法的并行多线程收集器。
而且是并行的多多线程收集器.java1.8默认的收集器.
特点:
Parallel Scavenge收集器的关注点与其他收集器不同, ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)
(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
主要使用场景
主要适合在后台运算而不是太多交互的任务/
比如需要与用户交互的程序,良好的响应速度能提升用户的
体验;而高吞吐量则可以最高效率的利用CPU时间,尽快的完成程序的运算任务等
15.Serial Old收集器
是Serial收集器的老年代版本,他同样是一个单线程收集器
16.Parallel Old收集器
Parallel Old都是Parallel收集器的老年代版本
17.CMS收集器
CMS收集器的GC周期由6个阶段组成。其中4个阶段(名字以Concurrent开始的)与实际的应用程序是并发执行的,而其他2个阶段需要暂停应用程序线程。
初始标记:为了收集应用程序的对象引用需要暂停应用程序线程,该阶段完成后,应用程序线程再次启动。
并发标记:从第一阶段收集到的对象引用开始,遍历所有其他的对象引用。
并发预清理:改变当运行第二阶段时,由应用程序线程产生的对象引用,以更新第二阶段的结果。
重标记:由于第三阶段是并发的,对象引用可能会发生进一步改变。因此,应用程序线程会再一次被暂停以更新这些变化,并且在进行实际的清理之前确保一个正确的对象引用视图。这一阶段十分重要,因为必须避免收集到仍被引用的对象。
并发清理:所有不再被应用的对象将从堆里清除掉。
并发重置:收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态。
一个常见的误解是,CMS收集器运行是完全与应用程序并发的。我们已经看到,事实并非如此,即使“stop-the-world”阶段相对于并发阶段的时间很短。
应该指出,尽管CMS收集器为老年代垃圾回收提供了几乎完全并发的解决方案,然而年轻代仍然通过“stop-the-world”方法来进行收集。对于交互式应用,停顿也是可接受的,背后的原理是年轻带的垃圾回收时间通常是相当短的。
18.G1收集器
G1收集器是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK 7u4版本发行时被正式推出,在JDK9中更被指定为官方GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短。G1收集器专门针对以下应用场景设计
可以像CMS收集器一样可以和应用并发运行
压缩空闲的内存碎片,却不需要冗长的GC停顿
对GC停顿可以做更好的预测
不想牺牲大量的吞吐量性能
不需要更大的Java Heap
不深入了,项目没做几个不看理论了,下次再深入。