【JVM内存结构+垃圾回收器GC+类加载器】

1. JVM

在这里插入图片描述

2. JVM内存结构

在这里插入图片描述

2.1 堆

  1. 概述:堆时Java虚拟机所管理的内存中最大的一块存储区域,主要用来存放使用new关键字创建的对象所有对象。实例以及数组都要在堆上分配,堆内存被所有线程共享。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)
    在这里插入图片描述
    在这里插入图片描述
  2. Java堆分为年轻代(Young Generation)和老年代(Old Generation),其中年轻代默认占整个堆空间的1/3,老年代默认占整个堆空间的2/3;年轻代又分为伊甸园(Eden)和幸存区(Survivor区),伊甸区占新生代空间的4/5,幸存者区占1/5;幸存区又分为From Survivor(S0)空间和 To Survivor(S1)空间,他们两个平分幸存者区的空间。
  3. 通常新生成的对象一般都是放在新生代的。如果数据体积过大的对象会直接存入老年代,另外,年龄超过设定的阈值后,也会从新生代转移到老年代中进行存储。
  4. 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OOM(OutOfMemoryError )异常。
  5. 我们可以通过设置Java堆的参数,优化减少FullGC:
    -Xms:设置堆的初始空间大小。
    -Xmx:设置堆的最大空间大小,一般不要大于物理内存的80%。
    -XX:NewSize(-Xns):设置新生代初始空间大小。
    -XX:MaxNewSize(-Xmn):设置新生代最大空间大小。
    -XX:SurvivorRatio=8 :年轻代中Eden区与Survivor区的容量比例值,默认为8表示两个Survivor
    :eden=2:8,即一个Survivor占年轻代的1/10

1.2 方法区

  1. 概述:方法区与堆有很多共性:线程共享,内存不连续,可扩展,可垃圾回收,同样当无法在扩展时会抛出OutOfMemoryError异常。正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)。
  2. 方法区的特点是,它存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  3. 方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。
    以HotSpot 虚拟机来说,在 JDK1.8 之前,方法区也被称作为永久代。在JDK7之前的HotSpot虚拟机中,纳入字符串常量池的字符串被存储在永久代中,因此导致了一系列的性能问题和内存溢出错误。JDK8之后就没有永久代这一说法变成叫做元空间(meta space)。元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而减少出现像永久代的内存溢出错误了,也能降低出现泄漏的数据移到交换区这样的情况。用户可以为元空间设置一个可用空间最大值,防止异常占用过多物理内存。

1.3 虚拟机栈

  1. Java 虚拟机的每一条线程都有自己私有的 Java 虚拟机栈,这个 Java 虚拟机栈跟线程同时创建,所以它跟线程有相同的生命周期。
    Java 虚拟机栈描述的是 Java 方法执行的内存模型:每一个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中的入栈到出栈的过程。
    在这里插入图片描述
  2. 局部变量表:(方法中定义的局部变量)局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
  3. 操作数栈:(方法中有需要运算的变量会进行栈)操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者。
  4. 动态链接:(当前方法的引用地址)Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
  5. 方法出口:(方法结束后要继续的位置)无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

1.4 本地方法栈

  1. 概述:和虚拟栈相似,只不过它服务于Native方法,线程私有。当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,会使用到本地方法栈。

1.5 程序计数器

  1. 概述:程序计数器是由CPU的寄存器实现的,不会发生内存溢出问题。程序计数器可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。
  2. 为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。
  3. 如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是
    Native 方法,计数器值为Undefined。

3. 垃圾回收器【GC】

  1. 概述:垃圾回收机制,守护线程

3.1 垃圾判断

  1. 概述:在 JVM 进行垃圾回收之前,首先就是判断哪些对象是垃圾,也就是说,要判断哪些对象是可以被销毁的,其占有的空间是可以被回收的。根据 JVM 的架构划分,我们知道, 在 Java 世界中,几乎所有的对象实例都在堆中存放,所以垃圾回收也主要是针对堆来进行的。
  2. 在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。
3.1.1 引用计数法:
  1. 概述:当对象被创建后,引用计数器就会+1,当对象失去引用后,引用计数器就会-1,如果当前计数器是0,则表示对象是垃圾对象,需要等待GC回收。
  2. 优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
  3. 缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。
3.1.2 可达性分析法:
  1. 概述:(GC Roots查找法【根搜索法】)从GC Roots根据引用关系向下搜索,走过的路叫引用链,如果一个对象在该引用链上没有出现,则称为不可达对象【垃圾对象】,需要等待GC回收。
  2. 在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:
    在这里插入图片描述
  3. 根集中的对象称之为GC Roots,也就是根对象。可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。被识别为不可达的对象,GC就可以进行回收删除。
  4. 优点:可以解决循环引用的问题,不需要占用额外的空间
  5. 缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用

3.2 垃圾回收算法

  1. 查看JDK使用垃圾回收器:java -XX:+PrintCommandLineFlags -version
  2. jdk8包括之前:Parallel
  3. jdk8之后用的是:G1

3.2.1 标记清除:

  1. 概述:在进行GC时,先把要垃圾对象打上标记,然后清除垃圾对象,把标记垃圾对象的内存释放出来。
    在这里插入图片描述
  2. 优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
  3. 缺点:标记和清除过程的效率都不高,这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片,虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。
    (释放的内存空间存在内存碎片的问题,如果要创建的对象大小超过了某个释放内存,则不能使用)

3.2.2 标记复制算法:

  1. 概述:把内存分为两块,一块使用,一块空着,在进行GC时,先标记垃圾对象,把存活对象移到空着的内存,释放之前使用的内存,避免了内存碎片。
    在这里插入图片描述
  2. 优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
  3. 缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。(使用空间小,只使用了一半)

3.2.3 标记整理算法:

  1. 概述:在进行GC时,先把要垃圾对象打上标记,然后清除垃圾对象,把标记垃圾对象的内存释放出来,把存活的对象进行整理,让剩余的内存连续分布。它标记的过程与“标记-清除”算法中的标记过程一样,但对标记后得出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。
    在这里插入图片描述
  2. 优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
  3. 缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

3.2.4 分代收集算法:

  1. 概述:分代收集(Generational Collector)算法的将堆内存划分为新生代和老年代。新生代又被进一步划分为Eden 和 Survivor 区,其中 Survivor 由 FromSpace(S0)和 ToSpace(S1)组成。所有通过new创建的对象的内存都在堆中分配。分代收集,是基于一个思想:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的分代采取不同的回收算法进行垃圾回收,以便提高回收效率。
    在这里插入图片描述
  2. 在分代收集算法中,对象的存储具有以下特点:
    1. 对象优先在 Eden 区分配。
    1. 大对象直接进入老年代。
    1. 长期存活的对象将进入老年代,默认为 15 岁。
  1. JVM分代年龄最大为什么是15?
    对于晋升老年代的分代年龄阈值,我们可以通过-XX:MaxTenuringThreshold参数进行控制。在这里,不知道大家有没有对这个默认的 15 岁分代年龄产生过疑惑,为什么不是 16 或者 17 呢?实际上,HotSpot 虚拟机的对象头其中一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit,官方称它为Mark word。
  2. 例如,在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的 32bit 空间中 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄(32位和64位的Hot Spot的分代年龄都占4bit),2bit 用于存储锁标志位,1bit 固定为 0,其中对象的分代年龄占 4 位,也就是从0000到1111,而其值最大为 15,所以分代年龄也就不可能超过 15 这个数值了。
  3. 分代收集算法的具体流程是:
    当新对象生成,Eden 空间申请失败(因为空间不足等),则会发起一次 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,也就是新生代、老年代都进行回收。

4. 类加载器

  1. 概述:类加载器的作用就是将磁盘上的class文件加载到虚拟机内存中。
  2. 类加载的过程:
  • 加载:将字节码文件通过IO流读取到JVM的方法区,并同时在堆中生成Class对像。
  • 验证:校验字节码文件的正确性。
  • 准备:为类的静态变量分配内存,并初始化为默认值;对于final static修饰的变量,在编译时就已经分配好内存了。
  • 解析:将类中的符号引用转换为直接引用。
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码。
    在这里插入图片描述

4.1 引导类加载器 Bootstrap

引导类加载器属于JVM的一部分,由C++代码实现。
引导类加载器负责加载 <JAVA_HOME>\jre\lib 路径下的核心类库,由于安全考虑只加载 包名 java、javax、sun开头的类。

4.2 扩展类加载器 ExtClassLoader

全类名: sum.misc.Launch$ExtClassLoader ,Java语言实现。
扩展类加载器的父加载器是 Bootstrap启动类加载器 (注:不是继承关系)
扩展类加载器负责加载 <JAVA_HOME>\jre\lib\ext 目录下的类库。

4.3 系统类加载器 AppClassLoader

全类名: sun.misc.Launcher$AppClassLoader
系统类加载器的父加载器是 ExtClassLoader扩展类加载器 (注: 不是继承关系)。
系统类加载器负责加载 classpath环境变量 所指定的类库,是用户自定义类的默认类加载器。

4.4 自定义类加载器

自定义类加载器是为了加载在jvm三个加载器负责的目录范围之外的类

5. 双亲委派机制

  1. 概述:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
  2. 优点:
    避免了类的重复加载,确保一个类的全局唯一性
    保证类的安全,防止核心的API 被随意篡改。
  3. 缺点:
    顶层的ClassLoader无法访问到底层的ClassLoader所加载的类
  4. Tomcat是如何打破双亲委派机制的:自定义类加载器
    在Tomcat中,类加载器所采用的加载机制和传统的双亲委派机制有一定的区别,当缺省的类加载器接受到一个类的加载任务时, 首先会由它自行加载 ,当他加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值