• JAVA虚拟机--JVM


    1. 内存结构

    请添加图片描述

    1.1 程序计数器

    1.1.1 概述

    在通用的计算机体系中,程序计数器用来记录当前正在执行的指令,在JVM中也是如此。程序计数器是线程私有,所以当一个新的线程创建时,程序计数器也会创建。由于Java是支持多线程,Java中的程序计数器用来记录当前线程中正在执行的指令。如果当前正在执行的方法是本地方法,那么此刻程序计数器的值为undefined。注意这个区域是唯一一个不抛出OutOfMemoryError的运行时数据区。

    1.1.2 总结

    1. 记录当前正在执行的指令,记住下一条JVM指令的执行地址
    2. 是线程私有的
    3. 是唯一一个不抛出OutOfMemoryError的运行时数据区

    1.2 虚拟机栈

    线程私有
    每个线程运行时所需要的内存,成为虚拟机栈
    每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    每个线程是能有一个活动栈帧,对应着当前正在执行的那个方法

    1.2.1 栈内存溢出

    1. 栈帧过多导致栈内存溢出
    2. 栈帧过大导致栈内存溢出

    1.2.2 可能抛出的错误

    栈内存的大小可以有两种设置,固定值和根据线程需要动态增长。
    在JVM栈这个数据区可能会发生抛出两种错误。

    1. StackOverflowError 出现在栈内存设置成固定值的时候,当程序执行需要的栈内存超过设定的固定值会抛出这个错误。
    2. OutOfMemoryError 出现在栈内存设置成动态增长的时候,当JVM尝试申请的内存大小超过了其可用内存时会抛出这个错误。

    1.2.3 总结

    1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。对象都存放在堆区中。
    2. 每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
    3. 栈分为3个部分:基本类型变量,执行环境上下文,操作指令区(存放操作指令).
    4. 在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
    5. 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

    1.3 本地方法栈

    一个支持native方法调用的JVM实现,需要有这样一个数据区,就是本地方法栈,Java官方对于本地方法的定义为methods written in a language other than the Java programming language,就是使用非Java语言实现的方法,但是通常我们指的一般为C或者C++,因此这个栈也有着C栈这一称号。一个不支持本地方法执行的JVM没有必要实现这个数据区域。本地方法栈基本和JVM栈一样,其大小也是可以设置为固定值或者动态增加,因此也会对应抛出StackOverflowError和OutOfMemoryError错误。
    在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。

    1.3.1 总结

    线程私有
    JVM调用本地方法时,给本地方法的一个内存空间
    本地方法: 被native修饰,没有方法体,底层是用c/c++编写

    1.4 堆

    1.4.1 Heap 堆

    堆数据区是用来存放对象和数组(特殊的对象)。堆内存由多个线程共享。堆内存随着JVM启动而创建。众所周知,Java中有一个很好的特性就是自动垃圾回收。垃圾回收就操作这个数据区来回收对象进而释放内存。如果堆内存剩余的内存不足以满足于对象创建,JVM会抛出OutOfMemoryError错误。

    1.4.2 特点

    它是线程共享的,堆中对象都需要考虑线程安全的问题
    有垃圾回收机制

    1.4.3 总结

    1. 存储的全部是对象,每个对象包含一个与之对应的class信息–class的目的是得到操作指令。
    2. jvm只有一个堆区(heap)被所有线程共享,堆区中不存放基本类型和对象引用,只存放对象本身。
    3. 堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。
    4. 缺点是,由于要在运行时动态分配内存,存取速度较慢。

    1.5 方法区

    方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

    在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

    在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

    2. 双亲委派机制

    当一个类加载器收到类加载请求的时候,不会立即去加载,而是让父类加载器去加载,如果父类也没有加载,继续往上抛出,直到被加载
    优点:避免了类重复加载
    **打断双亲委派:**重写findclass方法

    3. JVM之GC垃圾收集器

    3.1 GC垃圾回收机制

    3.1.1 垃圾回收方法

    首先GCROOT 会判断并标记对象是否需要被清理.

    1. 标记清理:

    把需要回收的对象直接回收,这样会导致内存碎片(回收的时候会产生内存间隙,比如: 回收掉两个1kb的对象,这样就会多出来两个不相邻的1kb空间,如果此时进来一个2kb的对象,就会放不进去)

    2. 标记整理:

    把需要回收的对象直接回收,然后所有对象位置前移,填补空间,这样会导致内存消耗太大

    3. 复制:

    把内存一分为二,在第一个内存中不需要回收的对象复制到第二个内存中,缺点:需要双倍内存 占空间

    3.2 JVM的GC实现

    分为yang年轻代和old老年代

    3.2.1 yang区

    该区分为 e伊甸园区 s0 s1 三个区,比例为8:1:1

    1. new的对象首先会放在伊甸园区
    2. 当伊甸园区满了以后,会使用gc的复制方法,把不回收的对象复制到s0区 然后把伊甸园区的垃圾对象回收掉。
    3. 当伊甸园区和s0区再次满了以后,就把不回收的对象复制到s1区,然后把伊甸园区和s0区的垃圾对象回收掉。
    4. s0区 、 s1区 搭配伊甸园区循环使用

    3.2.2 old区

    当垃圾回收超过六次依然没有被回收的对象,或者大对象会被放到old区
    当old区满了的时候,old区和yang区会同时进行gc(full gc 这时候会导致STW,STW:整个业务停摆去专门gc) ,full gc使用标记清理和标记整理方法
    为了避免yang区和old同时塞满导致的full gc,old区有一个回收阈值,可以自调节,当达到阈值时就会边放入对象边回收。

    3.3 对象漏标

    垃圾回收的并发标记阶段,gc线程和应用线程是并发执行的,所以一个对象被标记之后,应用线程可能篡改对象的引用关系,从而造成对象的漏标、误标,其实误标没什么关系,顶多造成浮动垃圾,在下次gc还是可以回收的,但是漏标的后果是致命的,把本应该存活的对象给回收了,从而影响的程序的正确性。

    为了解决在并发标记过程中,存活对象漏标的情况,GC HandBook把对象分成三种颜色(三色标记算法)

    3.4 三色标记算法

    垃圾回收线程间断扫描对象并完成标记,期间会有业务线程执行
    假设有三个对象,关系为: a的子类b b的子类d

    3.4.1 三色

    黑色: 自身以及可达对象都已经被标记
    灰色: 自身被标记,可达对象还未标记
    白色: 还未被标记

    3.4.1 算法过程

    当垃圾回收线程再次回来的时候,会跳过黑色标记a,去扫描灰色b和白色d

    如果在业务线程执行过程中 灰色b指向白色d的引用消失了(引用=null),并且把白色d引用指向黑色a,这个时候就会出现回收bug: 这个时候的垃圾回收线程是不知道以用已经改变了的,他以为白色d是垃圾,就会把d回收,这个时候业务代码a.d就会空指针

    所以,漏标的情况只会发生在白色对象中,且满足以下任意一个条件:

    1. 并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象
    2. 并发标记时,应用线程删除所有灰色对象到该白色对象的引用

    对于第一种情况,利用post-write barrier,记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍

    对于第二种情况,利用pre-write barrier,将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一遍

    CMS算法解决方案: 在每次操作对象引用的时候做一个写屏障(其实是切面),动态更新对象三色状态,避免回收引起的业务bug
    CMS会有一个remark阶段,必须从头到尾扫一遍,如果old区阈值满了会引起stw(业务停摆,全面gc)

    G1解决方案: 如果灰色和白色的引用消失了,会把这个引用指向放到专门的堆栈里,下次垃圾回收线程回来的时候先去看堆栈里有没有新加的引用指向,有的话再次扫描标记。

    使用三色 + G1 + SATB 可在并发标记过程中新分配对象不会漏标,边放对象边清理垃圾

    3.5 SATB 和G1

    3.5.1 SATB

    SATB全称snapshot-at-the-beginning,由Taiichi Yuasa为增量式标记清除垃圾收集器开发的一个算法,主要应用于垃圾收集的并发标记阶段解决了CMS垃圾收集器重新标记阶段长时间STW的潜在风险

    SATB保证了在并发标记过程中新分配对象不会漏标

    3.5.2 G1

    是region结构, 物理分区 逻辑分片
    吞吐量大
    响应速度200ms 对stw进行控制
    灵活:分region回收,优先回收花费时间少,垃圾比例高的region

    G1中如何解决漏标?

    1. 在引用关系被修改之前,插入一层 pre-write barrier
    2. 通过G1SATBCardTableModRefBS::enqueue(oop pre_val)把原引用保存到satb mark queue中,和RSet的实现类似,每个应用线程都自带一个satb mark queue.
    3. 在下一次的并发标记阶段,会依次处理satb mark queue中的对象,确保这部分对象在本轮GC是存活的

    4. JVM之ParNew垃圾收集器

    4.1 概述

    如果说Serial GC 是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本

    4.2 采用的机制:

    ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。
    ParNew收集器在年轻代中同样也是采用复制算法、“Stop-the-World”机制。

    4.3 默认垃圾收集器

    ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器。

    4.4 ParNew与 GC收集器回收效率

    由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial GC收集器更高效?

    ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快的完成垃圾收集,提升程序的吞吐量。

    但是在单个CPU的环境下,ParNew收集器不比Serial 收集器更高效,虽然Serial 收集器是基于串行回收,但是由于CPU不需要频繁的切换,因此可以有效避免多线程交互过程中产生的一些额外开销

    4.5 使用

    除了Serial外,目前只有ParNew GC能与CMS收集器配合工作。

    4.6 图解

    在这里插入图片描述

    4.7 使用

    1. 在程序中,开发人员可以通过选项“-XX:+UserParNewGC”手动指定使用ParNew收集器执行内存回收任务,它表示年轻代使用并行收集器,不影响老年代。

    2. -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数。

  • 相关阅读:
    comsol如何建立平面坐标系
    【论文阅读】Dense Passage Retrieval for Open-Domain Question Answering
    Pandas
    Android图片圆角转换 RoundedImageView开源项目 小记(1)
    【SA8295P 源码分析 (二)】37 - OpenWFD Server 启动流程 之 openwfd_server.c main 函数源码分析
    【JavaScript】Date对象(创建时间对象、常用Date方法总结)
    如何一次性解压多个文件
    netty群聊客户端服务器及心跳检测
    webfunny埋点漏斗功能
    python - 类和对象
  • 原文地址:https://blog.csdn.net/abcd741258358/article/details/126854131