• JVM-内存模型 面试总结


    目录

    1. Java内存区域

    1.1 五大内存区域

    1.2 对象的内存布局

    1.3 对象的访问定位

    2. 判断对象是否存活算法

    2.1 引用计数算法

    2.2 可达性分析算法(根搜索算法)

    3 垃圾收集算法

    3.1 标记-清除算法

    3.2 标记-复制算法

    3.3 标记/整理算法

    3.4 枚举根节点算法

    4. 垃圾收集器

    4.1 Serial

    4.2 ParNew收集器

    4.3 Parallel Scavenge

    4.4 Parallel Old

    4.5 CMS

    4.6 G1收集器

    5. 面试问题汇总

    5.1 GC是怎么判断对象是被标记的

    5.2 什么时候触发GC

    5.3 cms收集器是否会扫描年轻代

    5.4 什么是空间分配担保

    5.5 为什么复制算法要分两个Survivor,而不直接移到老年代

    5.6 stop the world具体是什么,有没有办法避免

    5.7 新生代什么样的情况会晋升为老年代

    5.8 怎么理解g1,适用于什么场景

    5.9 对象晋升规则

    5.10 堆内存分配区域

    5.11 finalize()方法

    5.12 内存屏障

    5.13 创建一个对象内存分配流程图

    5.14  双亲委派机制


    1. Java内存区域

            Java程序内存的分配是在JVM虚拟机内存分配机制下完成。
            Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
            方法区和堆是所有线程共享的,栈,本地方法栈和程序虚拟机则为线程私有的。

    1.1 五大内存区域

    程序计数器

            程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
    为什么需要程序计数器
            我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储
    注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

    Java栈(虚拟机栈)
    栈描述的是Java方法执行的内存模型。
            每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出,下图栈1先进最后出来】

    栈帧: 是用来存储数据和部分过程结果的数据结构。
    栈帧的位置:  内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里]
    栈帧大小确定时间: 编译期确定,不受运行期数据影响

    需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

    本地方法栈

            本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

     

            堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制
            注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代,再细致点还有Eden(伊甸园)空间之类的不做深究。
            根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

            Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
            分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。

    方法区

            方法区在JVM也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称,方法信息,字段信息)、静态变量、常量以及编译器编译后的代码等。在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
            在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非只有Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为”永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过JDK7之后,HotSpot虚拟机便将运行时常量池的永久代移除了。

    1.2 对象的内存布局

    在HotSpot虚拟机中。对象在内存中存储的布局分为
            1.对象头
            2.实例数据
            3.对齐填充

    对象头【markword】

    markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。
            为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息。
            假设当前为32bit,在对象未被锁定情况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。
            markOop中提供了大量方法用于查看当前对象头的状态,以及更新对象头的数据,为synchronized锁的实现提供了基础。[比如说我们知道synchronized锁的是对象而不是代码,而锁的状态保存在对象头中,进而实现锁住对象]。

    实例数据

            存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。
            分配策略:相同宽度的字段总是放在一起,比如double和long

    对齐填充

            由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。

    1.3 对象的访问定位

    对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
        1.句柄访问对象
        2.直接指针访问对象。(Sun HotSpot使用这种方式)

    句柄访问

    优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。

    直接指针

    与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样。
    优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。

    2. 判断对象是否存活算法

    2.1 引用计数算法

            早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
            优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
            缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。

    2.2 可达性分析算法(根搜索算法)

    目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
    它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。

    Root Set 就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。
    这种算法的基本思路:
    (1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
    (2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
    (3)重复(2)。
    (4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
    Java和C#中都是采用根搜索算法来判定对象是否存活的。


    可作为GC Roots的对象有四种
            ①虚拟机栈(栈桢中的本地变量表)中的引用的对象。
            ②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
            ③方法区中的常量引用的对象,
            ④本地方法栈中JNI(native方法)引用的对象

    标记阶段关键点

            (1)开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便JVM可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。
            (2)暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
            (3)在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:


    要真正宣告对象死亡需经过两个过程。
            1.可达性分析后没有发现引用链
            2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。

    3 垃圾收集算法

    三大垃圾收集算法
        1.标记/清除算法【最基础】
        2.复制算法
        3.标记/整理算法

    3.1 标记-清除算法

    标记-清除算法是最基础的垃圾收集算法。
    算法分为两个阶段,标记和清除。
    首先标记出需要回收的对象,标记完成后,统一回收掉被标记的对象。
    当然可以反过来,先标记存活的对象,统一回收未被标记的对象。
    标记-清除 两个缺点是,执行效率不稳定和内存空间的碎片化问题。

    3.2 标记-复制算法

            1969年 Fenichel提出“半区复制”,将内存容量划分对等两块,每次只使用一块。当这一块内存用完,将还存活的对象复制到另外一块,然后把已使用过的内存空间一次清理掉~
            1989年,Andrew Appel提出“Appel式回收”,把新生代划分为较大的Eden和两块较小的Survivor空间。每次分配内存只使用Eden和其中一块Survivor空间。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上。Eden和Survivor比例是8:1:1
    “半区复制”缺点是浪费可用空间,并且,如果对象存活率高的话,复制次数就会变多,效率也会降低。

    3.3 标记/整理算法

            1974年,Edward 提出“标记-整理”算法,标记过程跟“标记-清除”算法一样,接着让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存~
            标记-清除算法和标记整理算法本质差异是:前者是一种非移动式的回收算法,后者是移动式的回收算法。
            是否移动存活对象都存在优缺点,移动虽然内存回收复杂,但是从程序吞吐量来看,更划算;不移动时内存分配更复杂,但是垃圾收集的停顿时间会更短,所以看收集器取舍问题。
    Parallel Scavenge收集器是基于标记-整理算法的,因为关注吞吐。CMS收集器是基于标记-清除算法的,因为它关注的是延迟。


    jvm采用`分代收集算法`对不同区域采用不同的回收算法。
            新生代采用复制算法
            老年代采用标记/清除算法或标记/整理算法
     

    3.4 枚举根节点算法

    GC Roots 被虚拟机用来判断对象是否存活

    可作为GC Roos的节点主要是在一些全局引用【如常量或静态属性】、执行上下文【如栈帧中本地变量表】中。

    虚拟机先得知哪些地方存放对象的引用,在类加载完时。HotSpot把对象内什么偏移量什么类型的数据算出来,在jit编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息。【目前主流JVM使用准确式GC】
    OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。但是也存在一个问题,可能导致引用关系变化。
    这个时候有个safepoint(安全点)的概念。
    HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。 GC时对一个Java线程来说,它要么处在safepoint,要么不在safepoint。
    safepoint不能太少,否则GC等待的时间会很久
    safepoint不能太多,否则将增加运行GC的负担

    4. 垃圾收集器

    年轻代收集器
    Serial、ParNew、Parallel Scavenge
    老年代收集器
    Serial Old、Parallel Old、CMS收集器
    特殊收集器
    G1收集器[新型,不在年轻、老年代范畴内]

     新生代收集器

    4.1 Serial

            Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。

    4.2 ParNew收集器

            可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同。

    优点:
            1.支持多线程,多核CPU下可以充分的利用CPU资源
            2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】

    缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,

    老年代收集器

    4.3 Parallel Scavenge

            和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法"

    4.4 Parallel Old

            支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"【老年代的收集器大都采用此算法】

    4.5 CMS

    CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】
    CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发(Concurrent)的
    它的运作分为4个阶段
            1.初始标记:标记一下GC Roots能直接关联到的对象,速度很快
            2.并发标记:GC Roots Tarcing过程,即可达性分析
            3.重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记
            4.并发清除
    以上初始标记和重新标记需要stw(停掉其它运行java线程)
    之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。
    总体上CMS是款优秀的收集器,但是它也有些缺点。
            1.cms堆cpu特别敏感
            2.cms无法处理浮动垃圾(采用"标记-清除“算法)

    4.6 G1收集器

            G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。

            G1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。
    用到的算法为标记-清理、复制算法


            G1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。
            G1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。
            G1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。

    5. 面试问题汇总

    5.1 GC是怎么判断对象是被标记的

            通过枚举根节点的方式,通过jvm提供的一种oopMap的数据结构,简单来说就是不要再通过去遍历内存里的东西,而是通过OOPMap的数据结构去记录该记录的信息,比如说它可以不用去遍历整个栈,而是扫描栈上面引用的信息并记录下来。
            总结:通过OOPMap把栈上代表引用的位置全部记录下来,避免全栈扫描,加快枚举根节点的速度,除此之外还有一个极为重要的作用,可以帮HotSpot实现准确式GC【这边的准确关键就是类型,可以根据给定位置的某块数据知道它的准确类型,HotSpot是通过oopMap外部记录下这些信息,存成映射表一样的东西】。

    5.2 什么时候触发GC

            简单来说,触发的条件就是GC算法区域满了或将满了。
    minor GC(young GC):当年轻代中eden区分配满的时候触发[值得一提的是因为young GC后部分存活的对象会已到老年代(比如对象熬过15轮),所以过后old gen的占用量通常会变高]
    full GC:
            ①手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
            ②发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
            ③老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。
            ④CMS GC时出现Promotion Faield[pf]
            ⑤统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。
    这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FUll GC。
            比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC(这点挺智能的)
            Promotion Faield:minor GC时 survivor space放不下[满了或对象太大],对象只能放到老年代,而老年代也放不下会导致这个错误。
            Concurrent Model Failure:cms时特有的错误,因为cms时垃圾清理和用户线程可以是并发执行的,如果在清理的过程中
    可能原因:
            1 cms触发太晚,可以把XX:CMSInitiatingOccupancyFraction调小[比如-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)]
            2 垃圾产生速度大于清理速度,可能是晋升阈值设置过小,Survivor空间小导致跑到老年代,eden区太小,存在大对象、数组对象等情况
            3.空间碎片过多,可以开启空间碎片整理并合理设置周期时间

    5.3 cms收集器是否会扫描年轻代

    会,在初始标记的时候会扫描新生代。
    虽然cms是老年代收集器,但是我们知道年轻代的对象是可以晋升为老年代的,为了空间分配担保,还是有必要去扫描年轻代。

    5.4 什么是空间分配担保

            在minor gc前,jvm会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果是的话,则minor gc可以确保是安全的,
            如果担保失败,会检查一个配置(HandlePromotionFailire),即是否允许担保失败。
    如果允许:继续检查老年代最大可用可用的连续空间是否大于之前晋升的平均大小,比如说剩10m,之前每次都有9m左右的新生代到老年代,那么将尝试一次minor gc(大于的情况),这会比较冒险。
            如果不允许,而且还小于的情况,则会触发full gc。【为了避免经常full GC 该参数建议打开】
            这边为什么说是冒险是因为minor gc过后如果出现大对象,由于新生代采用复制算法,survivor无法容纳将跑到老年代,所以才会去计算之前的平均值作为一种担保的条件与老年代剩余空间比较,这就是分配担保。
            这种担保是动态概率的手段,但是也有可能出现之前平均都比较低,突然有一次minor gc对象变得很多远高于以往的平均值,这个时候就会导致担保失败【Handle Promotion Failure】,这就只好再失败后再触发一次FULL GC,

    5.5 为什么复制算法要分两个Survivor,而不直接移到老年代

            这样做的话效率可能会更高,但是old区一般都是熬过多次可达性分析算法过后的存活的对象,要求比较苛刻且空间有限,而不能直接移过去,这将导致一系列问题(比如老年代容易被撑爆)
    分两个Survivor(from/to),自然是为了保证复制算法运行以提高效率。

    5.6 stop the world具体是什么,有没有办法避免

            stop the world简单来说就是gc的时候,停掉除gc外的java线程。
            无论什么gc都难以避免停顿,即使是g1也会在初始标记阶段发生,stw并不可怕,可以尽可能的减少停顿时间。

    5.7 新生代什么样的情况会晋升为老年代

    对象优先分配在eden区,eden区满时会触发一次minor GC
    对象晋升规则
            1 长期存活的对象进入老年代,对象每熬过一次GC年龄+1(默认年龄阈值15,可配置)。
            2 对象太大新生代无法容纳则会分配到老年代
            3 eden区满了,进行minor gc后,eden和一个survivor区仍然存活的对象无法放到(to survivor区)则会通过分配担保机制放到老年代,这种情况一般是minor gc后新生代存活的对象太多。
            4 动态年龄判定,为了使内存分配更灵活,jvm不一定要求对象年龄达到MaxTenuringThreshold(15)才晋升为老年代,若survior区相同年龄对象总大小大于survior区空间的一半,则大于等于这个年龄的对象将会在minor gc时移到老年代

    5.8 怎么理解g1,适用于什么场景

            G1 GC 是区域化、并行-并发、增量式垃圾回收器,相比其他 HotSpot 垃圾回收器,可提供更多可预测的暂停。增量的特性使 G1 GC 适用于更大的堆,在最坏的情况下仍能提供不错的响应。G1 GC 的自适应特性使 JVM 命令行只需要软实时暂停时间目标的最大值以及 Java 堆大小的最大值和最小值,即可开始工作。
            g1不再区分老年代、年轻代这样的内存空间,这是较以往收集器很大的差异,所有的内存空间就是一块划分为不同子区域,每个区域大小为1m-32m,最多支持的内存为64g左右,且由于它为了的特性适用于大内存机器。
    适用场景:
            1.像cms能与应用程序并发执行,GC停顿短【短而且可控】,用户体验好的场景。
            2.面向服务端,大内存,高cpu的应用机器。【网上说差不多是6g或更大】
            3.应用在运行过程中经常会产生大量内存碎片,需要压缩空间【比cms好的地方之一,g1具备压缩功能】。

    5.9 对象晋升规则

            1 长期存活的对象进入老年代,对象每熬过一次GC年龄+1(默认年龄阈值15,可配置)。
            2 对象太大新生代无法容纳则会分配到老年代。
            3 eden区满了,进行minor gc后,eden和一个survivor区仍然存活的对象无法放到(to survivor区)则会通过分配担保机制放到老年代,这种情况一般是minor gc后新生代存活的对象太多。
            4 动态年龄判定,为了使内存分配更灵活,jvm不一定要求对象年龄达到MaxTenuringThreshold(15)才晋升为老年代,若survior区相同年龄对象总大小大于survior区空间的一半,则大于等于这个年龄的对象将会在minor gc时移到老年代。

    5.10 堆内存分配区域

    1.年轻代(Young Generation)

            几乎所有新生成的对象首先都是放在年轻代的。新生代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。大部分对象在Eden区中生成。当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC)。回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空, 如此往复。当Survivor1区不足以存放 Eden和Survivor0的存活对象时,就将存活对象直接存放到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。
    2.年老代(Old Generation)

            在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。比如:
            byte[] data = new byte[4*1024*1024]
            这种一般会直接在老年代分配存储空间。
    当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。
    3.持久代(Permanent Generation)

            用于存放静态文件(class类、方法)和常量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
            永久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。


    堆内存分配策略明确以下三点
            (1)对象优先在Eden分配。
            (2)大对象直接进入老年代。
            (3)长期存活的对象将进入老年代。


    对垃圾回收机制说明以下三点
            新生代GC(Minor GC/Scavenge GC):发生在新生代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁(不一定等Eden区满了才触发),一般回收速度也比较快。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
            老年代GC(Major GC/Full GC):发生在老年代的垃圾回收动作。Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
            新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

     

    5.11 finalize()方法

            在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源。但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象以释放资源,这个方法就是finalize()。它的原型为:
    protected void finalize() throws Throwable
            在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throws Throwable表示它可以抛出任何类型的异常。
            意义:之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候Java允许在类中定义一个finalize()方法。


    特殊的区域例如:
            1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法。这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free()方法是在C/C++中的函数,所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。
            2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。
    换言之,finalize()的主要用途是释放一些其他做法开辟的内存空间,以及做一些清理工作。因为在Java中并没有提够像“析构”函数或者类似概念的函数,要做一些类似清理工作的时候,必须自己动手创建一个执行清理工作的普通方法,也就是override Object这个类中的finalize()方法。比如:销毁通知。
            一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。
    JAVA里的对象并非总会被垃圾回收器回收。1 对象可能不被垃圾回收,2 垃圾回收并不等于“析构”,3 垃圾回收只与内存有关。也就是说,并不是如果一个对象不再被使用,是不是要在finalize()中释放这个对象中含有的其它对象呢?不是的。因为无论对象是如何创建的,垃圾回收器都会负责释放那些对象占有的内存。
            当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。

    5.12 内存屏障

    内存屏障:在不同CPU执行的不同线程对同一个变量的缓存值不同。
    为什么需要内存屏障
            由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题.
    简单来说:
            1.在不同CPU执行的不同线程对同一个变量的缓存值不同,为了解决这个问题。
            2.用volatile可以解决上面的问题,不同硬件对内存屏障的实现方式不一样。java屏蔽掉这些差异,通过jvm生成内存屏障的指令。
    对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。


    内存屏障的作用
    cpu执行指令可能是无序的,它有两个比较重要的作用
            1.阻止屏障两侧指令重排序
            2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

    5.13 创建一个对象内存分配流程图

    1, 对象一般是在Eden区生成。
    2, 如果Eden区填满,就会触发Young GC。
    3, 触发Young GC的时候,Eden区实现清除,没有被引用的对象直接被清除。
    4, 依然存活的对象,会被送到Survivor区,Survivor =S0+S1.,
    5, 每次Young GC时,存活的对象复制到未使用的那块Survivor 区,当前正在使用的另外一块Survivor 区完全清除,接着交换两块Survivor 区的使用状态。
    6, 如果Young GC要移送的对象大于Survivor区上限,对象直接进入老年代。,
    7, 一个对象不可能一直呆在新生代,如果它经过多次GC,依然活着,次数超过-XX:MaxTenuringThreshold的阀值,它直接进入老年代。简言之就是,对象经历多次滚滚长江,红尘世事,终于成为长者(进入老年代)
     

    5.14  双亲委派机制

    什么是双亲委派机制
            JVM通过双亲委派机制对类进行加载。双亲委派机制指的是一个类在收到类加载请求后,他自己先不会去尝试加载这个类,而是把这个类加载向上委派给父类加载器,然后一层一层的向上委派,直到启动类加载器。然后如果父类加载器无法加载这个类,它就会向下委派给子类加载器,直到这个类加载成功。当然了,如果到最后的加载器都无法找到该类,就会报出ClassNotFoundException异常。

    双亲委派机制的作用
            双亲委派机制保障了类的唯一性和安全性。试想下,每个类的加载都要先经过父类的加载,只有父类没有的情况下,才会交给子类,这样就保证了类的唯一性。还有可以设想下,要是我们自己也创建了一个java.lang.String类,如果不是因为双亲委派机制,启动类加载器已经加载过了java.lang.String类,之后不会再加载,那么作为java核心类的String类就完全可能被篡改,所以双亲委派机制还保证了类的安全性。


    Java双亲委派机制的破坏
    打破双亲委派机制的场景:JDBC、JNDI、Tomcat等
    JDBC为什么需要破坏双亲委派机制
            原因是原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
    Tomcat为什么要打破双亲委派机制
            首先tomcat是一个web容器,主要是需要解决以下问题
            一个web容器可能要部署两个或多个应用程序,不同的应用程序之间可能会依赖同一个第三方类库的不同版本,因此要保证每个应用程序的类库都是独立的、相互隔离的。
            部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM中。
            web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离。
            web容器支持jsp文件修改后不用重启,jsp文件也要编译成.class文件的,支持HotSwap功能。

    推荐两个有关JVM写得很好的文章:

    https://www.jianshu.com/p/76959115d486

    https://www.jianshu.com/p/5261a62e4d29

  • 相关阅读:
    LeetCode力扣刷题——妙用数据结构
    taichi库记录
    git提交代码冲突
    SpringCloud链路追踪SkyWalking-第二章-部署搭建及高可用
    【K8S】集群组件有什么?每个作用是什么?各组件之间关系
    Linux修改主机名
    如何使用navicat图形化工具远程连接MariaDB数据库【cpolar内网穿透】
    计算机毕业设计Java教师管理系统(源码+系统+mysql数据库+lw文档)
    SpringCloud——服务网关——GateWay
    BPF:BCC工具 funccount 统计内核函数调用(内核函数、跟踪点USDT探针)认知
  • 原文地址:https://blog.csdn.net/qq_41982570/article/details/126004006