zoukankan      html  css  js  c++  java
  • JVM GC和类加载机制

    Java内存模型

     线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

    JVM内存结构

    扩展

    栈:Java栈总是和线程关联在一起,每当创建一个线程,JVM就会为此线程创建一个Java栈;Java栈中有许多栈帧,栈帧与方法关联在一起,每运行一个方法就会创建一个栈帧。

    垃圾回收算法

    标记清除法

    先标记出需要回收的对象,然后一次性回收。缺点:会产生内存碎片,并且效率也不高。

    标记压缩法

    先标记出需要回收的对象,然后让存活对象向一端移动,移动的过程中进行回收辣鸡。避免了内存碎片问题。

    复制算法

    把内存划分出相等的两块,每次只用其中一块。当一块用完了,就将还存活的对象移动到另一块内存上,然后把辣鸡清理掉。内存分配不用来考虑碎片问题,只需要顺序分配即可。缺点:总有一块内存闲着没事干。

    如图

    默认年轻代和老年代比例为1:2,Eden区和存活区的比例为8:1:1

    年轻代GC(Minor GC):发生在年轻代的垃圾收集动作。

    老年代GC(Major GC):发生在老年代的垃圾收集动作。

    Full GC:对整个堆空间的垃圾收集。

    什么样的对象需要回收

    JVM主要靠“可达性分析”来判定对象是否存活的。也就是从一系列 GC根作为起点向下搜索,如果从GC根到这个对象可达,证明这个对象是存活对象,否则如果不可达,则证明这是个需要回收的对象

    可以作为GC根的对象:

    1. 虚拟机栈中引用的对象,具体来说是栈帧的局部变量表

    2. 方法区中类静态属性引用的对象

    3. 方法区中常量引用的对象

    4. Native方法引用的对象

    关于可达性分析,如下图所示,两个红色的对象虽然互相引用,但是从GC根到它们不可达,所以依然是可回收对象。

    垃圾回收过程

    首先新的对象都会放在Eden区,当Eden区没有足够空间进行分配时,会触发一次Minor GC,把存活的对象放在其中一个存活区S0。当Eden区再次填满,除了把Eden区中存活对象移动到S1中,S0中的存活对象也会被移动到S1,并删除垃圾对象,S0腾空。之后的垃圾回收会重复此过程。这里为什么要用两个存活区,主要为了解决内存碎片问题。

    当一个对象被重复回收达到一定阈值之后仍然存活,则将此移动到老年代。当老年代空间使用达到一定的阈值,则会触发针对老年代的收集Major GC。

    对于CMS收集器,老年代回收启动的阈值为92%(JDK1.6之前是68%)

    对于G1收集器,老年代回收启动的阈值是45%

    进入老年代的条件:

    • YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。
    • 经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
    • 动态年龄判定规则,To Survivor区中相同年龄的对象,如果其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
    • 大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。

    触发FullGC条件:

    • 无法容纳新晋升上来的对象时,会触发FullGC。
    • Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FullGC。
    • System.gc() 或者Runtime.gc() 被显式调用时,触发FullGC。

    程序排查FullGC:

    • 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
    • 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
    • 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. 
    • 动态生成了很多新类,使得 Metaspace 不断被占用,先引发FGC,最后导致OOM.
    • 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
    • JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。

    永久代的垃圾回收

    hotspot的方法区存放在永久代中,因此方法区被人们称为永久代。永久代的垃圾回收主要包括类型的卸载和废弃常量池的回收。

    对于常量池:Java1.7之前,不断将新常量添加到方法区,会导致方法区溢出。Java1.7中,运行时常量池已从永久代移除,转移到堆中,不断添加新常量的方法不再导致方法区溢出。Java1.8开始,废弃了永久代,取而代之的是一个元数据区的存储空间。

    对于类信息:在大量使用反射、动态代理CGLib等字节码框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

    在G1收集器中,只有进行Full GC才会触发永久代的回收,反过来,永久代满了之后也会触发Full GC。

    Java8为什么废弃方法区(永久代)?

    永久代经常不够用导致内存溢出或者发生内存泄漏。

    类方法信息的大小难于确定,也就是说永久代大小的指定很困难。

    元空间(MetaSpace)

    元空间是方法区的实现,主要用于存储类信息、静态变量等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

    元空间的本质和永久代类似,都是对JVM规范中方法区的实现。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

    垃圾回收器

    串行收集器

    串行辣鸡收集器只用一个单线程做所有工作,其内存占用空间大小也是所有辣鸡收集器里最低的。

    • 新生代辣鸡回收使用复制算法
    • 老年代使用标记压缩算法

    并行收集器

     

    并行收集器相对于串行收集器,使用了多线程来完成辣鸡回收的工作,但是同样也需要Stop-The-World。新生代和老年代的回收都是并行的。

    (历史:刚引入的时候,新生代使用多线程,而老年代则是单线程进行辣鸡回收。随着堆的尺寸和老年代对象的数量和大小不断增长,老年代辣鸡回收的时间不断变长,增加了一个多线程的老年代收集器和多线程的新生代收集器同时使用的方式,由此得到了增强的并行辣鸡收集器)

    • -XX:+UseParallelGC  新生代使用并行收集器,老年代使用串行收集器
    • -XX:+UseParallelOldGC  新生代和老年代都使用并行收集器
    • 新生代使用复制算法
    • 老年代使用标记压缩算法

    CMS收集器(Concurrent Mark Sweep)

     

    在CMS垃圾回收中,新生代的回收与并行垃圾收集器很类似。它们是多线程的并且会使应用程序线程暂停。主要区别在于老年代的收集上。

    CMS做垃圾回收的时候与应用线程同时进行,除了少数的相对短暂的GC同步暂停,可以说是大多数情况是并发进行的。

    CMS老年代收集活动从初始标记开始,这个阶段标记GC根可以直接关联到的对象,这个阶段是要暂停应用线程的。之后进入并发标记阶段,这个阶段标记所有存活对象,和应用线程一同运行。接着,进入重新标记阶段,这个阶段主要处理初始标记,并发标记过程中可能错过的对象,这个阶段是要暂停应用线程的。最后,并发清除启动,释放所有死亡对象所占用的空间。

    • 挑战1:要在应用消耗完Java的可用堆之前完成并发收集工作,因此选择一个合适的时机来启动这个并发收集工作尤为重要。
    • 挑战2:处理老年代中的空间碎片。如果老年代中空间碎片太小,无法容纳刚晋升上来的对象,因为CMS并发收集循环中并不执行压缩,所以可能导致CMS回过来使用串行GC,触发一次full收集,导致一个漫长的暂停。
    • 缺点:1. 老年代主要使用标记清除算法,不进行压缩,清理碎片显得很重要。2. 不能处理浮动垃圾,也就是在回收过程中新产生在标记过程之后的垃圾无法处理。
    • -XX:+UseCMSCompactAtFullCollection   垃圾收集完成后,进行一次内存碎片整理
    • -XX:CMSFullGCsBeforeCompaction  回收一定次数后,压缩一次内存

    JDK 8中两个主要的并发收集器:
      并发标记扫描(CMS)收集器:此收集器适用于喜欢较短垃圾收集暂停且可以与垃圾收集共享处理器资源的应用程序(并发收集&低停顿)。
      Garbage-First垃圾收集器:这种服务器式收集器适用于具有大内存的多处理器机器。它以高概率满足垃圾收集暂停时间目标,同时实现高吞吐量。

    G1收集器(Garbage First)

    G1 是一种低延时的垃圾回收器,旨在避免Full GC。G1把Java堆拆成一系列分区,这样的话,在某一个时间段内,大部分辣鸡回收只在一个区内而不是整个堆中进行。区域大小可以从1 MB到32 MB不等,具体取决于堆大小,每个分区的大小必须是2的幂。目标是不超过2048个分区。伊甸区,幸存区和老年代是这些地区的逻辑集合,并不是连续的。

    年轻代默认是整个Java堆尺寸的5%,最大60%;

    默认老年代堆空间占用超过45%,就会启动一次老年代收集。巨型对象:大小≥分区空间50%的对象。G1里的full GC使用的是与串行收集器相同的算法。发生full GC时,执行对整个内存堆的全面压缩。

    一个新概念:新生代不再是一个连续内存块,一个分区既可以变成新生代,也可以变成老年代。

    堆被划分为一组大小相同的堆区域,每个区域都是一个连续的虚拟内存区域。G1执行一个并发全局标记阶段,以确定整个堆中对象的活性。

    标记阶段完成后,G1知道哪些区域大部分是空的。它首先收集这些区域,这通常会产生大量的自由空间。这就是为什么这种垃圾收集方法被称为Garbage-First。

    G1将对象从堆的一个或多个区域复制到堆上的单个区域,并且在此过程中压缩并释放内存。这个过程是多线程执行,以减少暂停时间并提高吞吐量。因此,随着每次垃圾收集,G1不断努力减少碎片。这超出了以前两种方法的能力->CMS(Concurrent Mark Sweep)垃圾收集不进行压缩。并行压缩仅执行整堆压缩,这会导致相当长的暂停时间。

    所有Eden区+幸存区=新生代

    ..

    新生代的收集和前面的没啥区别,都要暂停应用线程。老年代比CMS的老年代收集还要与众不同。↓↓

    一个G1并发周期包含:初始标记、并发根分区扫描、并发标记、重新标记和清除。

    在G1中,一旦达到内存堆的占用阈值[yu zhi],一次并发stop-the-world方式的初始标记阶段就会被安排执行,在此阶段标记所有GC根,根是对象图的起点。这个阶段会跟着下一次新生代收集同时进行。然后进入并发根分区扫描,扫描和跟踪survivor分区里所有对象的引用,唯一的限制是在下一次GC前必须先完成扫描,因为一次新的GC会产生一个新的存活对象集合,它跟初始标记的存活对象是有区别的。

    然后进入并发标记阶段,标记老年代中所有存活对象。当并发标记阶段结束,并行stop-the-world的重新标记阶段就被启动,标记那些因为在标记阶段同时执行的应用线程导致产生的错过的对象。重新标记结束,就执行清除阶段,优先回收没有任何存活对象的分区,然后把每个收集过辣鸡的分区中的存活对象转移到一个可用分区中,一旦存活对象被转移,那么这个分区(新生代或者老年代)就可以被回收为可用分区。

    G1最大的暂停时间来源于 新生代收集和混合收集(新生代和老年代一起)

    特点:

    • 低延时收集器,旨在避免full GC
    • G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大。
    • 尽可能多的回收垃圾(Garbage First),采用启发式收集算法,在老年代找出具有高收集收益的分区进行收集(CMS则会在将要耗尽内存时候再回收).
    • 无内存碎片:与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-压缩”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

    收集器搭配

    Serial:-XX:+UseSerialGC

    ParNew:-XX:+UseParNewGC

    Parallel Scavenge :-XX:+UseParallelGC

    Parallel Old :-XX:-UseParallelOldGC(替代Serial Old)

    CMS:-XX:+UseConcMarkSweepGC

    G1:-XX:+UseG1GC

    为什么JVM要有类加载机制?

    1. 任意一个类,在JVM中必须是唯一的存在。基于双亲委派模型设计的类加载器,经过层层传递,加载请求最终都会被BootstrapClassLoader所响应,保证了类的唯一性。

    2. 考虑到安全因素,Java核心API中定义的类不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心API发现存在名字的类并且该类已被加载,则不会加载不明来源的java.lang.Integer,而是直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

    类加载过程

    主要为五个阶段:加载、验证、准备、解析、初始化。

    1、加载

    ”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:

    (1)通过一个类的全限定名来获取其定义的二进制字节流

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

    (3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。

    相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。

    2、验证

    验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:

    (1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。

    (2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。

    (3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。

    (4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。

    对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。

    3、准备

    准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:

    (1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,

    (2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如

    public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。

    4、解析

    解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号引用和直接引用呢?

    • 符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
    • 直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。

    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

    5、初始化

    这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。

    在初始化阶段,主要为类的静态变量赋予正确的初始值(JVM负责对类进行初始化,主要对静态变量进行初始化)。在Java中对类变量进行初始值设定有两种方式:

    ①声明类变量是指定初始值

    ②使用静态代码块为类变量指定初始值

    JVM初始化步骤

    1、假如这个类还没有被加载和连接,则程序先加载并连接该类

    2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

    3、假如类中有初始化语句,则系统依次执行这些初始化语句

    类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

    1. 创建类的实例,也就是new的方式
    2. 访问某个类或接口的静态变量,或者对该静态变量赋值
    3. 调用类的静态方法
    4. 反射(如 Class.forName(“com.shengsiyuan.Test”))
    5. 初始化某个类的子类,则其父类也会被初始化
    6. Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类
  • 相关阅读:
    思维导图
    网络面经
    2.17 C++ 专项练习 错题复盘
    C++面经
    2.15 C++专项练习 错题复盘
    uboot下读取flash,上传tftp服务器、下载
    Hi3516EV100烧录出厂固件
    用Hi3518EV200板当spi烧录器
    生而为人,我很抱歉!深夜爬虫, 我很抱歉 ,附微信 “ 网抑云” 公众号爬虫教程!
    阿里HR: 你会 Android 实现侧滑菜单-design吗? CN看了,原来这么简单呀!
  • 原文地址:https://www.cnblogs.com/LUA123/p/9831729.html
Copyright © 2011-2022 走看看