《深入理解Java虚拟机:JVM高级特性与最佳实践》第三版 第2.1-3.3章节学习笔记

一、Java内存区域与内存溢出异常
  • Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。其中程序计数器、虚拟机栈和本地方法栈是线程私有的隔离数据区;方法区和Java堆区是由所有线程共享的数据区

    1. 程序计数器:是一块较小的内存空间,如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空。当处理器执行指令时,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    2. Java虚拟机栈:是线程私有的,它的生命周期与线程相同。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

    3. 本地方法栈:与虚拟机栈所发挥的作用是非常相似的,本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。本地方法栈也会分别抛出StackOverflowError和OutOfMemoryError异常。

    4. Java堆:Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例(以及数组)都在这里分配内存。所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。

    5. 方法区:各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError异常。(在jdk8以前,为方便省去给方法区编写内存管理代码的工作。HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区。到了jdk 8,完全废弃了永久代的概念,改用与在本地内存中实现的元空间(Metaspace)来代替,把J永久代的内容全部都移到了元空间中。)

    6. 运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池具备动态性,运行期间也可以将新的常量放入池中。

    7. 直接内存:它不是虚拟机运行时数据区的一部分,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作,避免了 在Java堆和Native堆中来回复制数据。可能导致OutOfMemoryError异常出现。

  • 为对象分配内存:类加载检查通过后,虚拟机将为新生对象分配内存。分配内存有三种方式:

    1. 指针碰撞:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,为对象分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。在并发情况下虚拟机是采用CAS配上失败重试的方式保证更新指针操作的原子性。
    2. 空闲列表:如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
    3. 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完 了,分配新的缓存区时才需要同步锁定
  • 对象分配到的内存空间都被初始化为了零值,所有的字段都为默认的零值,对象需要的其他资源和状态信息需要按照预定的意图构造好。所以一般来说,new指令之后会接着执行 ()方法,即执行构造方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

  • 在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头实例数据对齐填充

    1. 对象头

      • HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它 为“Mark Word”。

      • 对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例。

    2. 实例数据:是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。HotSpot虚拟机默认的分配顺序会将相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。

    3. 对齐填充:这并不是必然存在的,它仅仅起着占位符的作 用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

  • 对象的主流访问方式主要有使用句柄直接指针两种:

    • 如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息;
    • 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。
  • 虚拟机调试参数总结

    1. -Xms20m/-Xmx20m*:设置堆的最小值为20m/最大值为20m
    2. -XX:+HeapDumpOnOutOf-MemoryError:让虚拟机 在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
    3. -Xss128k:每个线程的虚拟机栈大小设置为128k,栈容量的最小值会受到操作系统内存分页大小的限制。
    4. -Xoss128k:每个线程的本地方法栈大小设置为128k
    5. -XX:PermSize=6M -XX:MaxPermSize=6M:限制JVM永久代的大小为6M
    6. -XX:MaxMetaspaceSize=10M:Java8之后才有,设置元空间最大值,默认-1,即只受限于本地内存大小。
    7. ·-XX:MetaspaceSize=5M:Java8之后才有,指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载。
    8. -XX:MinMetaspaceFreeRatio:设置在垃圾收集之后控制最小的元空间剩余容量的百分比
    9. -XX:Max-MetaspaceFreeRatio:用于控制最大的元空间剩余容量的百分比。
    10. -XX:MaxDirectMemorySize=10M:设置直接内存的容量大小为10M。
二、垃圾收集器与内存分配策略(前三小节)
  • Java内存运行时区域中的程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。因此这几个区域就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。

  • Java堆方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。

  • 判断对象是否已死的两种算法:

    1. 引用计数算法:这个看似简单 的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
    2. 可达性分析算法:通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连, 即从GC Roots到这个对象不可达,则证明此对象是不可能再被使用的。
  • 可作为GC Roots的对象包括:在虚拟机栈(栈帧中的本地变量表)中引用的对象、在方法区中类静态属性引用的对象、在方法区中常量引用的对象(字符串常量池里的引用)、在本地方法栈(Native方法)引用的对象、Java虚拟机内部的引用、所有被同步锁(synchronized关键字)持有的对象等。

  • 虚引用也称为“幽灵引用”,它是最弱的一种引用关系。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将回收该对象。
    如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的 队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法。

  • 方法区的垃圾收集主要回收两部分内容:废弃的常量不再使用的类型

    1. 回收废弃常量:已经没有任何对象引用常量池中的常量,且虚拟机中也没有其他地方引用这个字面量。
    2. 回收不再使用的类型需要满足三个条件:
      • 该类所有的实例都已经被回收
      • 加载该类的类加载器已经被回收
      • 该类对应的java.lang.Class对象没有在任何地方被引用
  • 垃圾收集算法:

    1. 分代收集算法:将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。垃圾收集器每次只回收其中某一个或者某些部分的区域 。

      在分代收集算法中,对象之间会存在跨代引用,因而在新生代区域内的收集时,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性。为了解决这种问题,只需在新生代上建立一个全局的数据结构(记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

    2. 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

      主要缺点有两个:第一个是执行效率不稳定,如果 Java堆中包含大量对象被回收,会导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    3. 标记-复制算法:将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

      这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点。因此出现了把新生代分为一块较大的Eden空间和两块较小的 Survivor空间的划分方法(8:1:1的比例,并且需要分配担保)。

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值