深入理解 JVM (阅读总结)更新到第8章

本文深入探讨Java内存管理机制,包括自动内存管理、垃圾收集器、内存分配策略及类加载机制。涵盖Java堆、虚拟机栈、方法区等运行时数据区域,以及Serial、ParNew、CMS、G1等垃圾收集器的原理与应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

<注: 需要电子书可以到下方留下你的邮箱>

第一部分:走进JAVA

JAVA发展史:

1991.4 由 Sun 公司开发 Oak →

1995.5 Oak 改名为Java→

2006.11Sun公司宣布Java开源→

2009.4 Oracle收购了Sun

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

在这里插入图片描述
第二章 java内存区域与内存异常

<注: 以下红色标记异常为面试中易考点>

  1. 运行时数据区域
  • 程序计数器:一块较小的内存空间,可看作当前线程所执行的字节码的行号指示器,如果正在执行的Native方法,这个计数器值则为空(Undefined),唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况区域

  • Java虚拟机栈:每个方法执行的同时创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,Java虚拟机规范中,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常,如果扩展时无法申请到足够的内存,抛出 OutOfMemoryError 异常。

  • 本地方法栈:与虚拟机栈发挥作用相似,不同在于虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务,与虚拟机一样会抛出 StackOverflowError OutOfMemoryError

  • Java堆:Java虚拟机所管理的内存最大的一块,被所有线程共享,此内存唯一目的存放对象实例。也是垃圾收集器管理主要区域,被称“GC堆”。如果在堆中没有内存完成实例分配,并且堆也无法在扩展,抛出 OutOfMemoryError

  • 方法区:被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。别名:(非堆),很多人称为“永久代”。在方法区无法满足内存分配需求时,抛出 OutOfMemoryError

  • 运行时常量池:方法区一部分,用于存放编译期生成的各种字面量和符号引用,在方法区无法满足内存分配需求时,抛出 OutOfMemoryError

  • 直接内存:不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,在JDK1.4后新加入了NIO,可以试用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作,动态扩展不够会出现 OutOfMemoryError

  1. hotspot虚拟机对象
  • 对象的创建

  • 如下:

    虚拟机遇到一条New指令,先检查指令参数能否在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过,如果没有,先执行相应类的加载过程,类加载检查后,接下来虚拟机会为新生对象分配内存,把一块确定大小的内存从Java堆中划分出来。执行New指令之后接着执行方法,把对象按照程序员的意愿初始化。

  • “指针碰撞”:假设Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅
    是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

  • “空闲列表”:如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候
    从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

  • 对象的内存布局:

  • 对象在内存中存储的布局分为3块区域:
    1.对象头:包括两部分:对象哈希码、对象分代年龄等 第二部分:类型指针
    2.实例数据
    3.对齐填充

  • 对象的访问定位

  • 句柄访问:Java堆将会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,
    而句柄中包含了对象实例数据与类型数据各自的具体地址信息

  • 直接指针访问: reference中存储的直接就是对象地址

区别:前者对象被移动,只改表句柄中的实例数据指针,reference不需要修改,后者则速度更快。


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

1. 判断对象死活

  • 引用技术算法

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

  • 可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。

  • 引用:
  1. 强引用:类似 Object obj = new Object() 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

  2. 软引用:有用但非必须对象,在系统发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,将会抛出内存溢出异常。

  3. 弱引用:也是有用但非必须对象,比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存足够,都会回收掉只被弱引用关联的对象。

  4. 虚引用:也称幽灵引用,最弱的一种引用关系,无法对其生存时间构成影响,也无法通过虚引用来取得一个对象建立关联即可。

  • 生存死亡:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,他将会被第一次标记,并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize(0方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,当对象被判没有
    必要执行,这个对象将会放置在F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行,如果对象要拯救自己–只要重新与引用链上的。

<注:任何一个对象的finelize()方法都会被系统自动调用一次>

  • 回收方法区

  • 主要回收- “废气常量”其有以下几个特点:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例

  2. 加载该类的 ClassLoader已经被回收

  3. 该类对应的java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。


2. 垃圾收集算法

  • 标记-清除算法
  1. 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

  2. 两个不足:效率问题,标记和清除两个过程的效率都不高,另一个是空间问题,标记清除之后会产生大量不连续碎片,空间碎片太多导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

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

  2. 优点:在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除 中导致的引用更新问题。

  3. 缺点: 会造成一部分的内存浪费。

  • 标记-整理算法
  1. 首先标记出所有需要回收的对象,让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
  • 分代收集算法
  1. 根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,根据各个年代的特点采用适当的收集算法,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,采用复制算法,只要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记整理”算法来进行回收

3.垃圾收集器

  • Serial 收集器
  1. 最基本、发展历史最悠久的收集器,单线程收集器,当它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。在Client模式下的虚拟机来说是一个很好的入选择。

  2. JVM Server模式与client模式启动,最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升.原因是:当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,服务起来之后,性能更高.

  • ParNew 收集器
  1. 其实就是Serial收集器的多线程版本,运行在Server模式下的虚拟机中首选的新生代收集器。其中一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。在单CPU的环境中绝对不会有比Serial更好的效果。

  2. XX:UseParNewGC :启用ParNew收集器。

  3. XX:ParalletGCThreads :设定并行垃圾收集的线程数量。

  4. 默认开启的线程数等于cpu数。

  5. 并行(Parallel): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。》

  6. 并发(Concurrent): 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。》

  • Parallel Scavenge 收集器
  1. 是一个新生代收集器,采用复制算法的收集器,特点:目标是达到一个可控制的吞吐量。

  2. 吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /
    +垃圾收集时间),虚拟机总运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。》

  3. XX:GCTimeRatio 设置吞吐量大小

  4. XXMaxGCPauseMilis 控制最大垃圾收集停顿时间

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

  2. 如果在Server模式下,主要有两大用途,一种是在JDK1.5 Parallel Scavenge 收集器搭配使用,另一种用途是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

  • Parallel Old 收集器
  1. Parallel Old 是 Paraller Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scabenge 加 Parallerl Old 收集器。
  • CMS 收集器
  1. 一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现。整个过程分为以下四个步骤 初始标记、并发标记、重新标记、并发清除,其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。

  2. 缺点:

    对CPU资源敏感,CMS默认启动的回收线程数是(cpu数量+3)/4。所以CPU数量少会导致用户程序执行速度降低较多。

    有空间碎片:CMS是一款基于“标记-清除”算法实现的,所以会产生空间碎片。CMS提供了-XX:UseCMSCompactAtFullCollection开发参数用于开启内存碎片的合并整理,由于内存整理是无法并行的,所以停顿时间会变长。还有-XX:CMSFullGCBeforeCompaction,这个参数用于设置多少次不压缩Full GC后,跟着来一次带压缩的(默认为0)。

    浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然会有新垃圾产生,这部分垃圾得标记过程之后,所以CMS无法在当收集中处理掉他们,只好留待下一次GC清理掉,这一部分垃圾称为浮动垃圾。

  • G1 收集器
  1. 一款面向服务端应用的垃圾收集器。与其他GC收集器相比,具以下特点:

    并行与并发 :G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPUT或者CPU核心)来缩短Stop-The-World停顿的时间,其他部分收集器需要停顿Java线程执行的GC动作,G1收集器任然可以通过并发的方式让Java程序继续运行。

    分代收集:与其他收集器一样,分代概念在G1中依然得以保留。

    空间整合:G1从整体来看是基于“标记-整理”算法实现的收集器,从局部看是基于“复制”算法实现的。

    可预测的停顿:降低停顿时间是G1和CMS共同的关注点,但G1除了追求停顿外,在能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

    使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老生代的概念,但新生代和老生代不再是物理隔离的了,他们都是一部分Region (不需要连续)的集合

    步骤

    初始标记

    并发标记

    最终标记

    筛选回收

4. 内存分配与回收策略

jvm区域总体分两类,heap区和非heap区。heap区又分:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代-养老区)。 非heap区又分:Code Cache(代码缓存区)、Perm Gen(永久代)、Jvm Stack(java虚拟机栈)、Local Method Statck(本地方法栈)。

  • HotSpot虚拟机GC算法采用分代收集算法:

    1. 一个人(对象)出来(new 出来)后会在Eden Space(伊甸园)无忧无虑的生活,直到GC到来打破了他们平静的生活。GC会逐一问清楚每个对象的情况,有没有钱(此对象的引用)啊,因为GC想赚钱呀,有钱的才可以敲诈嘛。然后富人就会进入Survivor Space(幸存者区),穷人的就直接kill掉。

    2. 并不是进入Survivor Space(幸存者区)后就保证人身是安全的,但至少可以活段时间。GC会定期(可以自定义)会对这些人进行敲诈,亿万富翁每次都给钱,GC很满意,就让其进入了Genured Gen(养老区)。万元户经不住几次敲诈就没钱了,GC看没有啥价值啦,就直接kill掉了。

    3. 进入到养老区的人基本就可以保证人身安全啦,但是亿万富豪有的也会挥霍成穷光蛋,只要钱没了,GC还是kill掉。

    非heap区域中Perm Gen中放着类、方法的定义,jvm Stack区域放着方法参数、局域变量等的引用,方法执行顺序按照栈的先入后出方式。


第四章 虚拟机性能监控与故障处理工具

  • jconsole 通过JDK/bin 目录下“jconsole.exe” 即可启动

  • visualvm 需要下载

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

第六章 类文件结构

可以参考一下 https://blog.csdn.net/A_zhenzhen/article/details/77977345 感觉这个写的更容易懂一些

第七章 虚拟机类加载机制

1. 类的加载时机

  • 遇到new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类初始化。

  • 当虚拟机启动时,用户需要指定一个要执行的类,虚拟机会先初始化这个类。

  • 当使用JDK1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,

并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

2. 类的加载过程

注: 类的整个生命周期包括: 加载 验证 准备 解析 初始化 使用 和卸载

  • 加载

    • 通过一个类的全限定名来获取定义此类的二进制字节流

    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    • 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问

    (注: HotSpot 对象Class比较特殊,它虽然是对象,但是它存放在方法区里面)

  • 验证

    目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。

    • 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。(例:魔数这一类的验证)

    • 元数据验证:对字节码描述的信息进行语义解析,以保证其描述的信息符合Java语言规范的要求(例:这个类的父类是否继承了不允许被继承的类)。

    • 字节码验证:通过数据流和控制流分析确定程序语法是合法的、符合逻辑的。(例:保证方法体中的类型转换是有效的)

    • 符号引用验证:确保解析动作能正常执行,如果无法通过符号引用验证,那么会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如 java.lang.IllegalAccessError java.lang.NoSuchFieldErrorjava.lang.NoSuchMethodError等。(例:符号引用中通过字符串描述的全限定名是否能找到对应的类)

  • 准备

    • 为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。
  • 解析

    • 虚拟机将常量池内的符号引用替换为直接引用的过程。
  • 初始化

    • 执行类构造器方法的过程。

3. 类加载器

  • 类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。

  • JVM有三种类加载器,当 JVM启动的时候,Java开始使用如下三种类加载器:

    • 根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

    • 扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

    • 系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

  • 类加载器加载Class大致要经过如下8个步骤:

    1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
    2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
    3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
    4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
    5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
    6. 从文件中载入Class,成功后跳至第8步。
    7. 抛出ClassNotFountException异常。
    8. 返回对应的java.lang.Class对象。
  • 类加载机制:

  • 1. JVM的类加载机制有如下3种。

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

2. 双亲委派机制:

  • 双亲委派机制 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

  • 双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

第八章 虚拟机字节码执行引擎

1. 运行时栈帧结构

  • 用于支持虚拟机进行方法调用和方法执行的数据结构。

  • 存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息,每一个方法从调用开始到执行完成的过程,都对应一个栈帧在虚拟机里面从入栈到出栈的过程。

  • 1.1 局部变量表:变量值存储空间,用于存放方法参数和方法内部定义的局部变量。以变量槽Variable Slot,坚诚 Slot 为最小单位,一个变量槽可以存放一个32位以内的数据类型,对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。

  • 1.2 操作数栈和局部变量表一样,32位数据类型所占的栈容量为1,,6位数据类型所占的栈容量为2.

  • 1.3 动态连接:因为 Java 是在运行期间动态链接的,所以为了支持动态链接,需要将方法区里面的符号引用转为直接引用(即:给出地址),这就叫动态链接

  • 1.4 方法返回地址:当一个方法开始执行后,两种方式退出,一种是执行引擎遇到任意一个方法返回的字节码指令,另一种是在方法执行过程中遇到了异常,并且这个异常没有在方法体类得到处理。

第四部分:程序编译与代码优化

1. javac编译器

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值