《深入理解java虚拟机》-读书笔记一

一、java内存区域和内存溢出异常

java虚拟机所管理的内存包括如下几个区域:

程序计数器

它是较小的内存空间,可以看做是当前线程执行的字节码的行号指示器。由于java多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个时刻一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个程序计数器,各条线程之间计数器互不影响。一般把这类内存称为“线程私有”的内存。

Java虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,都对应一个栈帧在虚拟机栈中入栈到出栈的过程。

java虚拟机规范中,对这个区域规定了两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverFlowError异常。如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryutOfError 异常。

本地方法栈

它与java虚拟机栈类似,区别在于java虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机使用到的native方法服务。与java虚拟机栈一样,它也会抛出 StackOverFlowError 和 OutOfMemoryutOfError 异常。

java堆

java堆是被所有线程共享的最大的一块内存区域。在虚拟机启动时创建,此内存区域的唯一目的是存放对象实例。几乎所有对象实例都在这里分配内存。

java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。根据java虚拟机规范,java堆处于物理不连续的内存空间,只要逻辑上是连续的即可。但是如果在堆中没有完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区

方法区域java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常亮、静态变量、即时编译器编译后的代码等。同样当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。

运行时常量池

它是方法区的一部分。Class文件除了有类的版本、方法、字段、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。

二、对象的创建

  • 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,并且检查这个这个符号引用代表的类是否已被加载、解析和初始化。如果没有必须执行类的加载过程。
  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。
  • 调用对象的 init 方法,这样一个真正可用的对象才算完全创造出来。

三、对象的访问定位

对象访问的方式是取决于虚拟机实现而定的,目前主流的访问方式有使用句柄和直接指针两种。

如果使用句柄,那么java堆中将划分一块内存来作为句柄池,reference中存放的就是对象的句柄地址。而句柄中包含了对象实例数据和类型数据的具体地址。

如果使用直接指针,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息。而reference存储的直接就是对象地址。

这两种方式各有优势,使用句柄访问的最大好处就是 reference 存储的就是稳定的句柄地址,对象移动只会改变句柄中实例数据指针,而 reference 本身不需要修改。

使用直接指针访问方式的好处是速度更快,它节省了一次指针定位的开销,Hotspot 就是使用的第二种方式进行对象访问的。

四、垃圾收集器和内存分配策略

java堆存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,首先要确定这些对象中那些还“存活”着,哪些已经死去。

而判断对象是否存活的算法包括下面两种:

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加 1;当引用失效时,计数器就减一;任何时刻计数器为0的对象就是不可能再被使用的。

客观的说,引用计数算法实现简单,判断效率也很高。虽然有一些比较著名的应用案例,但是至少在java虚拟机里没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

举例来说:

对象objA和 objB 都有字段 instance ,赋值指令 objA.instance = objB及 objB.instance = objA ,除此之外,对象再无其他引用,实际上两个对象不可能再被访问,但是他们都相互引用着对方,导致引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

可达性分析算法

这个算法的基本思路是通过一系列的称为“GC Roots”的对象作为起点。从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有一个引用链相连,则证明这个对象是不可用的。 即时在可达性分析算法中不可达的对象,也不是“非死不可”的,要真正宣告一个对象的死亡,至少要经过两次标记过程。

在主流的商用程序语言(Java  C#)的主流实现中,都是通过可达性分析判断对象是否存活的。

在java 语音中,可作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI 引用的对象

垃圾收集算法

标记-清除算法

算法分为 “标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这个算法是最基础的收集算法,后续的算法都是基于这种思想改进得到的。它的不足之处有两个:一个是效率问题,标记和清除的过程效率偶都不高;另外一个是空间问题,标记和清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致之后为大对象分配空间,无法找到足够连续的内存不得不再次触发垃圾回收动作。

标记-清除过程如图:

复制算法

这个算法的思路是将内存分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后把使用过的内存一次清理掉。这样使得每次都是对整个半区进行垃圾回收,内存分配时就不用考虑内存碎片等复杂问题。

复制算法过程如图:

标记-整理算法

标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

标记-整理算法过程如图:

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,它是根据对象存活的周期不同将内存划分几块。一般是把java堆分为新生代和老生代,这样就可以根据各个年代的特点采用不同的收集算法。在新生代中,每次垃圾收集都会发现有大量对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率较高,没有额外空间对他进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。

垃圾收集器

HotSpot虚拟机包含的垃圾收集器

上图展示了 7 中作用于不同分代的收集器,如果两个收集器之间存在连线,说明它们可以搭配使用。虚拟机所处的区域,表示它是属于新生代收集器还是老年代收集器。

这里我在网上找了一张图,标注了各个收集器出现的 jdk 版本,图片来源于网络。

Serial 收集器

Serial 收集器是最基本、发展最悠久的收集器,这个收集器是一个单线程的收集器,它在进行垃圾收集时,必须暂停其它所有的工作线程,知道它收集结束。但是这对很多应用来说都是难以接受的。

但是实际到现在为止,它依然是虚拟机运行在Client 模式下默认新生代收集器,因为它简单而高效(与其它收集器的单线程比)。

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余包括 Serial 收集器使用的控制参数、收集算法、对象分配规则等都与 Serial 收集器完全一样。

值得注意的是它是许多虚拟机在 Server 模式下首选的新生代收集器。并且除了 serial 收集器之外,目前只有它能与 CMS 收集器配合工作。

parallel Scavenge 收集器

这个一个新生代收集器,使用的是复制算法,又是并行的多线程收集器。parallel Scavenge 收集器的目标是达到一个可控制的吞吐量,吞吐量= 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了 100 分钟,垃圾收集花费了 1 分钟,那吞吐量就是  99% 。

parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数。MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,收集器将尽可能的保证内存回收话费达到时间不超过设定值。GCTimeRatio 参数值应当是一个大于 0 且小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程的收集器,使用“标记-整理”算法。这个收集器的意义在于给 Client 模式下的虚拟机使用。

运行示意图:

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。

在注重吞吐量以及CPU 资源敏感的场合,都可以优先考虑 Parallel Old 加 Parallel Scavenge 收集器。

Parallel Old 工作过程如图:

CMS 收集器

CMS 收集器是一种以获取最短停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S 系统的服务端上,这类应用尤其重视服务响应速度,以给用户带来较好的体验。CMS 收集器就很符合这类应用的要求。

CMS 收集器是基于“标记-清除”算法实现的,它的运作过程相对复杂,整个过程分为四个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

运行示意图:
​​​​

CMS 收集器是一款优秀的收集器,但是存在 3 个明显的缺点:

  • CMS 收集器堆CPU 资源非常敏感。在并发阶段,虽然它不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU 资源)而导致应用程序变慢,总吞吐量会降低。
  • CMS 收集器无法处理浮动垃圾,可能出现“ Concurrent Mode Failure ”失败而导致另一次 Full GC 的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然就会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法再当次垃圾收集中处理掉,只好留在下一次 GC 时清理。这一部分垃圾就称为“浮动垃圾”。
  • 还有一个缺点就是“标记-清除”算法带来的,标记和清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致之后为大对象分配空间,无法找到足够连续的内存不得不再次触发垃圾回收动作。为了解决这个问题,CMS 收集器提供了一个参数:-XX:UseCMSCompactAtFullCollection 开关参数,用于要进行 FullGC 时开启内存碎片的合并整理过程。还有一个参数:-XX:CMSFullGCsBeforeCompaction, 这个参数用于设置执行多少次不压缩的 FullGC 后,来一次碎片整理。

G1 收集器

G1 收集器是当今收集器技术发展的最前沿的成果之一。它是一款面向服务端应用的垃圾收集器。并且具备以下特点:

  • 并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,缩短停顿时间,并通过并发的方式让java程序继续执行。
  • 分代收集:它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
  • 空间整合:G1 从整体来看是基于“标记-整理”算法实现的收集器,从局部看是基于“复制”算法实现的,这就意味着G1 运作期间不会产生空间碎片。
  • 可预测的停顿:G1 能建立可预测的时间模型,让使用者指定一个长度为 M 毫秒的时间片段内,消耗在垃圾收集的时间不超过 N 毫秒。

G1 收集器运行示意图:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值