美文网首页
《深入理解Java虚拟机》读书笔记

《深入理解Java虚拟机》读书笔记

作者: 云师兄 | 来源:发表于2018-11-29 22:34 被阅读16次

    第二部分 自动内存管理机制

    第二章 Java内存区域与内存溢出异常

    运行时数据区域

    Java虚拟机在执行Java程序的过程中将内存划分为下面多个不同区域。

    程序计数器

    是当前线程所执行的字节码的行号指示器。Java多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。为了线程切换后能恢复到之前的正确位置,每个线程都需要有独立的程序计数器来记录行号。程序计数器这块内存区域是“线程私有”的内存,记录的是正在执行的虚拟机字节码指令的地址。

    Java虚拟机栈

    Java虚拟机栈也是线程私有的,生命周期和线程相同。它描述的是Java方法执行的内存模型。每个方法执行的同时会创建一个栈帧用于保存局部变量表部分。局部变量可以是各种基本数据类型和引用类型。

    本地方法栈

    虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。

    Java堆

    堆(Heap)是所有线程共享的一块内存区域,在虚拟机启动时创建,唯一目的就是存放对象实例。所有的对象实例以及数组都要在堆上分配。堆事垃圾收集器管理的主要区域,因此被称为GC堆。堆可以处于物理上不连续的内存空间中,通过-Xmx和-Xms控制。

    方法区

    与堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息,常量,静态变量。

    对象的创建

    对象的创建

    虚拟机遇到new指令时,先检查这个类是否已被加载,如果没有先执行类加载过程。加载通过后,在堆中给对象分配内存。

    对象的内存布局

    对象的内存布局分为三块区域:对象头Header,实例数据和对齐填充。

    • 对象头包括两个部分信息。第一部分用于存储对象自身的运行时数据,如hashCode等。第二部分是类型指针,指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。另外如果对象是一个数组,那在对象头上还记录了数据的长度大小。
    • 实例数据存储了真正的有效信息,如字段内容,无论是父类继承下来的还是子类中定义的。
    • 第三部分对齐填充并不是必然存在的,仅仅起到占位符的作用,因为对象的大小必须是8字节的倍数,所以如果有空余就用占位符区填充,实现对齐。

    第三章 垃圾收集器与内存分配策略

    第二章介绍了Java内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭,即内存在线程结束后随着回收了。而Java堆和方法区是线程公有的,所以垃圾收集器所关注的就是这部分内存。

    确定对象是否存活

    引用计数算法

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

    主流Java虚拟机没有选用引用计数算法来管理内存,因为它很难解决对象之间相互循环引用的问题。

    可达性分析算法

    通过一系列的称为"GC Roots"的对象作为起始点往下搜索,所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。

    垃圾收集算法

    标记-清除算法

    最基础的收集算法是标记-清除(mark-Sweep)算法,后续算法基于此改进而得。分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

    这个算法不足在于:

    • 效率不高
    • 清除后导致内存碎片,使得无法找到足够连续的内存。

    复制算法

    基于标记-清除算法,提高效率产生的算法。将内存分为两块,每次只使用其中一块,当一块用完后就复制到另一块,再将用过的内存一次清理掉。这样每次只回收其中一整块内存就行,也解决了内存碎片的问题。

    缺点在于可用内存减少了。

    标记-整理算法

    标记-整理算法标记过程和标记-清除算法过程一样,但后续过程不是直接对可回收对象清理,而是让所有存活的对象都移向一端,然后直接清理掉端边界外的内存。

    分代收集算法

    根据对象存活周期不同将内存分为几代。把Java堆分为新生代和老年代,根据各年代的特点采用最适当的收集算法。新生代对象少量存活,使用复制算法,老年代对象存活率高,使用标记-清除算法和标记-整理算法来进行回收。

    HotSpot算法实现

    3.4这节没看太懂,先TODO一下吧😂。

    垃圾收集器

    前面讲到了很多垃圾收集算法,垃圾收集器就是内存回收的具体实现(个人理解就是实现的产品,不同的虚拟机,不同的版本都有不同的垃圾收集器)。分别针对堆的新生代和老年代划分出各种收集器。

    Serial收集器

    它是一个单线程的收集器。它只有一条收集线程去完成收集工作,并且在收集过程中必须暂停其他所有的工作线程。这种“Stop the World”理念导致:垃圾回收时用户线程停顿。为解决这个问题,后产生了一些更优秀的收集器来缩短停顿时间。虽然Serial收集器有这个问题,但他还是Client模式下默认的新生代收集器。尽管有缺点,但是由于它没有线程交互的开销,专心做垃圾收集自然可以获取最高的单线程收集效率。

    ParNew收集器

    ParNew收集器是Serial收集器的多线程版本,其余行为和Serial收集器的实现完全一样。在单CPU环境下ParNew收集器不会比Serial收集器有更好的效果;在CPU非常多的环境下,可以使用--XX:ParallelGCThreads参数来限制垃圾收集的线程数。

    Parallel Scavenge收集器

    Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)所以吞吐量高就可以高效率地利用CPU时间。Parallel Scavenge提供了两个参数用于精确控制吞吐量:用-XX:MaxGCPauseMills参数控制最大垃圾收集停顿时间,或-XX:GCTimeRatio参数来直接垃圾收集时间占总时间的比率。

    Serial Old收集器

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

    CMS收集器

    CMS收集器是一种以获取最短回收停顿时间为目标的收集器。全名为Concurrent Mark Sweep可以看出CMS收集器是基于标记-清除算法实现的。

    G1收集器

    G1是一款面向服务端应用的垃圾收集器。

    内存分配与回收策略

    Java技术体系中所提倡的自动内存管理最终归结为给对象分配内存以及回收内存两部分。关于回收内存,已经通过垃圾收集器去实现,接下来要讨论的就是如何给对象分配内存。

    大多数情况下,对象在堆上新生代Eden区中分配。

    第三部分 虚拟机执行子系统

    第六章 类文件结构

    运行在Java虚拟机上的语言除了Java语言外还有一些其他语言,为能在Java虚拟机上运行他们都要编译成Class文件。Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序排列在Class文件中。

    • 每个Class文件的头4个字节称为魔数,它的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别是基于安全方面的考虑,因为文件扩展名可以随意改动。Class文件的魔数值为: 0xCAFEBABE(咖啡宝贝)。
    • 紧挨着魔数的4个字节存储的是Class文件的版本号:前两个字节是次版本号,后两个字节为主版本号。
    • 紧挨着主次版本号之后的是常量池入口。由于常量池中常量个数不固定,所以常量池入口先放了两个字节用于代表常量池中常量个数。常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值等。而符号引用则属于编译原理方面的概念,包含下面三类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。常量池中的每一个常量都有一个特定的数据结构,此结构的开始是一个字节的标志位,代表当前这个常量属于哪种常量类型,其次才保存这个常量的长度大小和实际的内容(字符串)。
    • 在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,如这个Class是类还是接口,是否为public,是否是abstract,是否为final等。
    • 访问标志后面保存的是类索引,父类索引和接口索引,Class文件中由这三项数据来确定这个类的继承关系:类索引确定了这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引描述这个类实现了哪些接口,最终通过这些索引去常量池中去寻找具体的全限定名字符串。
    • 在类索引后面保存的是字段表,用于描述接口或者类中声明的变量:字段的作用域(是不是public,private等),是实例变量还是静态变量(static),可变性(final)......。所以字段表中只保存了字段的描述信息,实际字段叫什么名称还是保存在常量池中,在字段表中保存了对应的引用,即通过引用在常量池中找到这个字段的名称和数据类型。
    • 字段表后面存放的是方法表。方法表中存放的方法的描述信息,类比于字段表,这里就不细说了。方法内部的代码存放在方法属性表里面。
    • 字段表和方法表都可以携带自己的属性表集合。如方法中编译好的代码(字节码)存放在方法表的Code属性内,如果是抽象类或者接口,由于方法内没有实现代码,就不会有Code属性。

    相关文章

      网友评论

          本文标题:《深入理解Java虚拟机》读书笔记

          本文链接:https://www.haomeiwen.com/subject/aolqcqtx.html