首页 > 系统相关 >JVM内存模型分析

JVM内存模型分析

时间:2023-02-17 20:23:17浏览次数:54  
标签:收集器 对象 模型 线程 内存 JVM 方法 加载

JVM

  1. JVM内存模型解析

    JVM内存模型:https://juejin.cn/post/7024358170642350093 https://www.cnblogs.com/blknemo/p/13296007.html

    线程独占:JVM栈,本地方法栈,程序计数器
    线程共享:堆,方法区

    img

    • JVM栈:
      又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法
      出口等信息.调用方法时执行入栈,方法返回式执行出栈.

    • 本地方法栈
      与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.

    • 程序计数器
      保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行
      Native方法时,程序计数器为空.

      程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

      由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

      如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域


    • JVM内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所欲的对象实例都会放在这里,当堆
      没有可用空间时,会抛出OOM异常.根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行
      垃圾的回收管理

    • 方法区:
      又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据.1.7的永
      久代和1.8的元空间都是方法区的一种实现

    • 直接内存(非运行时数据区)

      在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。

      显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

  2. JVM 中线程和堆栈的关系

    栈是线程私有的,每个线程都是自己的栈,每个线程中的每个方法在执行的同时会创建一个栈帧用于存局部变量表、操作数栈、动态链接、方法返回地址等信息。每一个方法从调用到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。其中局部变量表,存放基本类型(boolean、byte、char、short、int、float)、对象的引用等等,对象的引用不是对象实例本身,而是指向对象实例的一个指针。

    • 堆是线程共享的,所有的对象的实例和数组都存放在堆中,任何线程都可以访问。Java的垃圾自动回收机制就是运用这个区域的。

    • 方法区也是线程共享的,用于存放类信息(包括类的名称、方法信息、字段信息)、常量、静态变量以及即时编译器编译后的代码等等

  3. 什么是类加载器,类加载的过程

    类加载过程详解:https://juejin.cn/post/7027347671211835400

    根据《Java虚拟机规范 Java SE8》版规定,类或者接口的加载过程分为加载(Loading)、链接(Linking)、初始化(Initialization)。其中链接(Linking)又可以分为:验证(Verification)、准备(Preparation)、解析(Resolution)。

    1. 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
    2. 连接,连接又包含三块内容:验证、准备、初始化。
      1. 验证,文件格式、元数据、字节码、符号 引用验证;
      2. 准备,为类的静态变量分配内存,并将其初始化为默认值;
      3. 解析,把类中的符 号引用转换为直接引用 初始化,为类的静态变量赋予正确的初始值
    3. 初始化,new出对象程序中使用
    4. 卸载,执行垃圾回收

    类加载器:

    JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的 Java运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于Java的跨平台性,经过编译的 Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确 保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据 读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载 完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。

    最后JVM对类进行初始化,包括:

    1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;

    2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:

    • 根加载器(BootStrap)

    • 扩展加载器(Extension)

    • 系统加载器(System)

    • 用户自定义类加载器(java.lang.ClassLoader的子类)

    从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是 根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器 无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个 类加载器的说明:

    • Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);

    • Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;

    • System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器

    3).触发类加载的条件

    ①.遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段的时候(被final修饰,已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。

    ②.使用java.lang.reflect包的方法对类进行反射调用的时候。

    ③.当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先出发父类的初始化。

    ④.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

    ⑤.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出发初始化。

    4)为什么 static final 修饰的常量不会触发类加载

    由于为常量,故此类型的已经定义了值的常量,从准备(编译)阶段就已经被直接赋予了开发者定义的值,并没有进入到解析和初始化阶段,故此类中的其他静态变量和静态代码块都不会被调用

  4. 什么是双亲委派原则,为什么使用双亲委派原则,如何打破

    1. 双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器。父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载

      优点: 1. 避免类的重复加载 2. 避免Java的核心API被篡改

    2. 如何打破双亲委派

      使用Thread.currentThread().getContextClassLoader()获取系统类加载器

      public static <S> ServiceLoader<S> load(Class<S> service) {
      
          // Thread.currentThread().getContextClassLoader() 默认是系统类加载器
          ClassLoader cl = Thread.currentThread().getContextClassLoader();
          return ServiceLoader.load(service, cl);
      }
      
      
    3. 为什么打破双亲委派:https://juejin.cn/post/7090932342012772389

      • tomcat打破双亲委派:

        Tomcat打破了双亲委派机制,让每个WebApp ClassLoader加载自己目录下的class文件,没有向上传递给父加载器。Tomcat通过这样一套类加载机制,实现了各个Web应用之间,相同依赖版本的统一加载,不同依赖版本的分别加载以及随时热加载JSP文件的功能

      • JDBC打破双亲委派

        JDBC的Driver是定义在rt.jar包中的,但是它的实现通常是由不同的数据库厂商来完成的,如mysql-connetor-java。同时rt.jar包中的DriverManager类会加载每个Driver接口的实现类并管理它们。那么问题来了,根据类加载机制,某个类需要引用其它类的时候,虚拟机将会用这个类的classloader去加载被引用的类。既然DriverManager是由BootStrap ClassLoader加载,那么各个Driver的实现类理应也由BootStrap ClassLoader来加载。但很显然,Boostrap Classloader显然是无法加载到mysql-connetor-java的,二者明显不在一个目录里,直接报ClassNotFound。因此只能在DriverManager里强行指定下层的classloader来加载Driver实现类,而这就会打破双亲委派模型

  5. final类型的常量在什么阶段进行初始化:https://juejin.cn/post/6907940560066543624

    1. final修饰的实例属性,在实例创建的时候才会赋值。

    2. static修饰的类属性,在类加载的准备阶段赋初值,初始化阶段赋值。

    3. static+final修饰的String类型或者基本类型常量,并且使用字面量赋值时,JVM规范是在初始化阶段赋值,但是HotSpot VM直接在准备阶段就赋值了。

    4. static+final修饰的其他引用类型常量,赋值步骤和第二点的流程是一样的。

  6. 方法区和静态常量池分别存储的什么信息:https://juejin.cn/post/6844903731301974029

    方法区:

    • 方法区是线程共享的区域。

    • 方法区存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    • 方法区被很多人称为“永久代”,因为HotSpot团队选择把GC延伸至方法区。不过现在已经有放弃永久代并逐步改为采用Native Memory来实现方法区的规划了,在JDK1.7的HotSpot中,一把原本放在永久代中的字符串常量池移出。

    • 内存区域可以是物理上不连续的,但逻辑上是连续的,与堆内存是一致的。

    • 方法区因为总是存放不会轻易改变的内容,故又被称之为“永久代”。HotSpot也选择把GC分代收集扩展至方法区,但也容易遇到内存溢出问题。可以选择不实现垃圾回收,但如果回收就主要涉及常量池的回收和类的卸载。

    • 该区域无法满足内存分配需求时,会抛出OOM异常。

    常量池:

    class文件常量池

    我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References),这就是我们所说的class文件常量池。 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:

    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

    (每种常量类型的数据结构可以查看《深入理解java虚拟机》第六章的内容)

    1. class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。

    运行时常量池

    1. 运行时常量池是方法区中的一部分。
    2. Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池(Constant Pool Table)。
    3. 常量池中存储的是编译期生成的各种字面量和符号引用,在JDK1.6及其之前的版本,这部分内容都将在类加载完后进入到方法区的运行时常量池中存放。
    4. 运行时常量池相比于类常量池的不同特征在于,运行时常量池具备动态性,也就是说不一定要编译期的时候才能产生常量,也可以在运行时产生常量,比如在运行期间调用String.intern()方法,也可以将新的常量放入运行时常量池中。
    5. 运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

    string.intern()作用:

    检查字符串常量池中是否存在String并返回池里的字符串引用;若池中不存在,则将其加入池中,并返回其引用。 这样做主要是为了避免在堆中不断地创建新的字符串对象。

    字符串常量池

    1. 字符串常量池在每个VM中只有一份,存放的是字符串常量的引用值。
    2. 字符串常量池——string pool,也叫做string literal pool。
    3. 字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中。
    4. string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。

    对于string pool:

    在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享

  7. 栈的基本结构(局部变量表,操作数栈,动态链接,方法出口):https://juejin.cn/post/6844903983400632327

    栈与栈帧

    每一个方法的执行到执行完成,对应着一个栈帧在虚拟机中从入栈到出栈的过程。java虚拟机栈栈顶的栈帧就是当前执行方法的栈帧。PC寄存器会指向该地址。当这个方法调用其他方法的时候久会创建一个新的栈帧,这个新的栈帧会被方法Java虚拟机栈的栈顶,变为当前的活动栈,在当前只有当前活动栈的本地变量才能被使用,当这个栈帧所有指令都完成的时候,这个栈帧被移除,之前的栈帧变为活动栈,前面移除栈帧的返回值变为这个栈帧的一个操作数。

    栈帧

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。 一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作

    局部变量表

    • 局部变量表是变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在java编译成class文件的时候,就在方法的Code属性的max_locals数据项中确定该方法需要分配的最大局部变量表的容量。
    • 局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放32位(4 字节)以内的数据类型( boolean、byte、char、short、int、float、reference和returnAddress八种)
    • 对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
    • reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。
    • Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。 系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
    • 系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。

    操作数栈

    • 操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。

    • 操作数栈的每一个元素可用是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型占用的栈容量为2。

    • 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作(例如:在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的)。

    • 在概念模型里,栈帧之间是应该是相互独立的,不过大多数虚拟机都会做一些优化处理,使局部变量表和操作数栈之间有部分重叠,这样在进行方法调用的时候可以直接共用参数,而不需要做额外的参数复制等工作。重叠过程如图所示:

      img

    动态连接

    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。由于篇幅有限这里不再继续讨论解析与分派的过程,这里只需要知道静态解析与动态连接的区别就好。

    方法返回地址

    当一个方法开始执行后,只有两种方式可以退出这个方法:

    • 执行引擎遇到任意一个方法返回的字节码指令:传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法来返回指令决定,这种退出的方法称为正常完成出口。
    • 方法执行过程中遇到异常: 无论是java虚拟机内部产生的异常还是代码中thtrow出的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出的方式称为异常完成出口,一个方法若使用该方式退出,是不会给上层调用者任何返回值的。无论使用那种方式退出方法,都要返回到方法被调用的位置,程序才能继续执行。方法返回时可能会在栈帧中保存一些信息,用来恢复上层方法的执行状态。一般方法正常退出的时候,调用者的pc计数器的值可以作为返回地址,帧栈中很有可能会保存这个计数器的值作为返回地址。方法退出的过程就是栈帧在虚拟机栈上的出栈过程,因此退出时的操作可能有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者的操作数栈每条整pc计数器的值指向调用该方法的后一条指令。
  8. JVM的角度分析多态:https://developer.aliyun.com/article/950983 https://blog.csdn.net/zmh458/article/details/101075633

    https://blog.csdn.net/qq_27948811/article/details/103941188

    多态的必要条件:

    1、 继承

    2、 子类重写父类方法

    3、 父类引用指向子类对象

    想了解java多态的实现,需了解JVM的内存模型,我们都知道在类加载的时候会解析信息到方法区;方法区包含类变量,常量池,字段信息,方法信息等

    每个类都有方法表,方法表主要存放方法的指针引用;当两个类满足继承条件后,子类的方法表将会指向父类的方法表(也就包含了父类的方法),这时子类就存在指向父类方法的指针,当子类重写父类的方法,也有个指向自己方法的指针,这是就会替换父类的指针

  9. 从JVM内存结构的角度分析单例模型,为什么单例模式能保证对象唯一:https://juejin.cn/post/6844903907609559048

    所有单例模式需要处理得问题都是:

    将构造函数私有化
    通过静态方法获取一个唯一实例
    保证线程安全
    防止反序列化造成的新实例等。

    推荐使用:DCL、静态内部类、枚举
    单例模式优点

    只有一个对象,内存开支少、性能好(当一个对象的产生需要比较多的资源,如读取配置、产生其他依赖对象时,可以通过应用启动时直接产生一个单例对象,让其永驻内存的方式解决)
    避免对资源的多重占用(一个写文件操作,只有一个实例存在内存中,避免对同一个资源文件同时写操作)
    在系统设置全局访问点,优化和共享资源访问(如:设计一个单例类,负责所有数据表的映射处理)

    单例模式缺点

    一般没有接口,扩展难
    android中,单例对象持有Context容易内存泄露,此时需要注意传给单例对象的Context最好是Application Context

    从JVM内存结构的角度理解:

    由于单例模式通过静态方法或枚举的方式进行获取,如下图

    public class EagerSingleton {
        //饿汉单例模式
        //在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
        private static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化
        private EagerSingleton() 
        {
            //私有构造函数
        }
        public static EagerSingleton getInstance()    //静态,不用同步(类加载时已初始化,不会有多线程的问题)
        {
            return instance;
        } 
    }
    

    当类加载时已经完成了类的初始化,静态方法都会存放在方法区(永久代),new 的对象会存放在堆中,这时方法区的指针引用指向队中的实例,而方法区是不会被gc的,所以实例也不会被回收,形成了唯一性

  10. 堆的内存结构详解,什么是对象年龄,对象是怎么切换到其他代:https://blog.csdn.net/weixin_43967582/article/details/121866362

    1. 现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
    • java7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区 Young Genetation Space 新生区 Young/New
      又被划分为Eden区和Survivor区, Tenure generation space 养老区 Old/Tenure,Permanent Space 永久区 Perm
    • java8及以后堆内存逻辑上分为三部分:新生区+养老区+元空间,Young Genetation Space 新生区 Young/New
      又被划分为Eden区和Survivor区,Tenure generation space 养老区 Old/Tenure,Meta Space 元空间 Meta
    1. 年轻代和老年代

      存储在JVM中的java对象可以被划分为两类:
      一类是生命周期比较短的瞬间对象,这类对象的创建和消亡都非常迅速
      另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
      java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)
      其中年轻代又可以划分为Eden空间,Survivor0空间和Survivor1空间(有时也叫做from区,to区)

    2. 配置新生代与老年代在堆结构的占比

      • 默认-XX:NewRatio = 2,标识新生代占1,老年代占2,新生代占整个堆的1/3
      • 可以修改-XX:NewRatio = 4,标识新生代占1,老年代占4,新生代占整个堆的1/5

      在HotSpot中,Eden空间和另外两个Survivor空间的缺省所占的比例是8:1:1
      当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。比如-XX:SurvivorRatio=8
      几乎所有java对象都是在Eden区被new出来的
      绝大部分的java对象的销毁都在新生代进行
      IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的
      可以使用选项“-Xmn”设置新生代最大内存大小

    3. 什么是对象年龄

      对象年龄可以理解为对象在新生代里 eden 和 survivor0, survivor1 之间复制的次数. 在 java8 中, 默认是 15 次, 会回收到老年代.

      性能优化时, 我们的应用大多数其实不需要复制这么多次, 才进入老年代.

    4. Minor GC 的流程

      1.Minor GC开始前,Survivor To是空的,Survivor From 是有对象的存储的。

      2.Minor GC后,Eden的存活对象都copy到Survivor To中,Survivor From的存活对象也复制Survivor To中。其中所有对象的年龄+1。

      3.Survivor From清空,成为新的Survivor To,带有对象的Survivor To变成新的Survivor From,重复回到步骤1。

      4.当结果Survivor To对象的年龄年龄大于阈值时,会copy到老年代。

      java maxTenuringThreshold=15 意味着会经过15次的minor gc才会到达老年代

  11. 在什么情况会发生OOM,常用的分析工具的命令:https://blog.csdn.net/qq_40378034/article/details/107729565

    什么情况下发生OOM:https://heapdump.cn/article/248921

    堆溢出

    这种场景最为常见,报错信息:

    java.lang.OutOfMemoryError: Java heap space
    

    原因

    1、代码中可能存在大对象分配 2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

    解决方法

    1、检查是否存在大对象的分配,最有可能的是大数组分配 2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题 3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存 4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性

    永久代/元空间溢出

    报错信息:

    java.lang.OutOfMemoryError: PermGen space
    java.lang.OutOfMemoryError: Metaspace
    

    原因

    永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。

    JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:

    • 字符串常量由永久代转移到堆中
    • 和永久代相关的JVM参数已移除
      可能原因有如下几种:
    1. 在Java7之前,频繁的错误使用String.intern()方法
    2. 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
    3. 应用长时间运行,没有重启

    没有重启 JVM 进程一般发生在调试时,如下面 tomcat 官网的一个 FAQ:

    Why does the memory usage increase when I redeploy a web application?

    That is because your web application has a memory leak.

    A common issue are “PermGen” memory leaks. They happen because the Classloader (and the Class objects it loaded) cannot be recycled unless some requirements are met (). They are stored in the permanent heap generation by the JVM, and when you redeploy a new class loader is created, which loads another copy of all these classes. This can cause OufOfMemoryErrors eventually.

    (*) The requirement is that all classes loaded by this classloader should be able to be gc’ed at the same time.

    解决方法

    因为该 OOM 原因比较简单,解决方法有如下几种:

    1. 检查是否永久代空间或者元空间设置的过小
    2. 检查代码中是否存在大量的反射操作
    3. dump之后通过mat检查是否存在大量由于反射生成的代理类
    4. 放大招,重启JVM

    GC overhead limit exceeded

    这个异常比较的罕见,报错信息:

    java.lang.OutOfMemoryError:GC overhead limit exceeded
    

    原因

    这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。

    解决方法

    1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
    2. 添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
    3. dump内存,检查是否存在内存泄露,如果没有,加大内存。

    方法栈溢出

    报错信息:

    java.lang.OutOfMemoryError : unable to create new native Thread
    

    原因

    出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。

    解决方法

    1. 通过 -Xss 降低的每个线程栈大小的容量
    2. 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
    • /proc/sys/kernel/pid_max
    • /proc/sys/kernel/thread-max
    • maxuserprocess(ulimit -u)
    • /proc/sys/vm/maxmapcount

    非常规溢出

    下面这些OOM异常,可能大部分的同学都没有碰到过,但还是需要了解一下

    分配超大数组

    报错信息 :

    java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    

    这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。

    解决方法就是检查你的代码中是否有创建超大数组的地方。

    swap溢出

    报错信息 :

    java.lang.OutOfMemoryError: Out of swap space
    

    这种情况一般是操作系统导致的,可能的原因有:

    1. swap 分区大小分配不足;
    2. 其他进程消耗了所有的内存。

    解决方案:

    1. 其它服务进程可以选择性的拆分出去
    2. 加大swap分区大小,或者加大机器内存大小

    本地方法溢出

    报错信息 :

    java.lang.OutOfMemoryError: stack_trace_with_native_method
    

    本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。

    这个异常出现的概率极低,只能通过操作系统本地工具进行诊断,难度有点大,还是放弃为妙

  12. 如何判断对象是垃圾,什么是GC ROOT:https://blog.csdn.net/canot/article/details/51037938

    一、GC如何判断一个对象为”垃圾”的
    java堆内存中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。那么GC具体通过什么手段来判断一个对象已经”死去”的?

    1. 引用计数算法(已被淘汰的算法)
      给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

    目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象
    之间相互循环引用的问题。尽管该算法执行效率很高。

    1. 可达性分析算法
      目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

    img

    二、GC回收的对象

    在Java语言中,可作为GC Roots的对象包括下面几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

    三、被GC判断为”垃圾”的对象一定会回收吗

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)

    如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

    finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

    四、什么是GC ROOT:https://zhuanlan.zhihu.com/p/134575094 https://blog.csdn.net/bolg_hero/article/details/79344745

    GC Root 的特点

    • 当前时刻存活的对象!

    GC Root 在哪里

    • 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
    • VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。

    这里有个问题? 为什么需要将GC root 设置为 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用?

    原因很简单,GC Root 需要确保引用所指的对象都是活着的,而当前线程 frame 中的对象,在这一时刻是存活的。

  13. GC 常用垃圾回收器有哪些,分别采用了哪些算法,G1的优势是什么:https://ost.51cto.com/posts/3543

    1. 新生代
      新生代采用复制算法,主要的垃圾收集器有三个,Serial、Parallel New 和 Parallel Scavenge,特性如下:

    Serial:单线程收集器,串行方式运行,GC 进行时,其他线程都会停止工作。在单核 CPU 下,收集效率最高。
    Parallel New:Serial 的多线程版本,新生代默认收集器。在多核 CPU 下,效率更高,可以跟CMS收集器配合使用。
    Parallel Scavenge:多线程收集器,更加注重吞吐量,适合交互少的任务,不能跟 CMS 配合使用。

    1. 老年代

      Serial Old:采用标记-整理(压缩)算法,单线程收集。
      Parallel Old:采用标记-整理(压缩)算法,可以跟 Parallel Scavenge 配合使用
      CMS:Concurrent Mark Sweep,采用标记-清除算法,收集线程可以跟用户线程一起工作。
      CMS缺点:吞吐量低、无法处理浮动垃圾、标记清除算法会产生大量内存碎片、并发模式失败后会切到Serial old。

    G1:把堆划分成多个大小相等的Region,新生代和老年代不再物理隔离,多核 CPU 和大内存的场景下有很好的性能。新生代使用复制算法,老年代使用标记-压缩(整理)算法。

    Serial收集器

    这是个单线程收集器,发展历史最悠久的收集器,当它在进行垃圾收集工作的时候,其他线程都必须暂停直到垃圾收集结束(Stop The World)。

    虽然Serial收集器存在Stop The World的问题,但是在并行能力较弱的单CPU环境下往往表现优于其他收集器;因为它简单而高效,没有多余的线程交互开销;Serial对于运行在Client模式下的虚拟机来说是个很好的选择

    使用-XX:+UseSerialGC参数可以设置新生代使用这个Serial收集器

    ParNew收集器
    ParNew收集器是Serial收集器的多线程版本;除了使用了多线程进行垃圾收集以外,其他的都和Serial一致;它默认开始的线程数与CPU的核数相同,可以通过参数-XX:ParallelGCThreads来设置线程数。

    从上面的图可以看出,能够与CMS配合使用的收集器,除了Serial以外,就只剩下ParNew,所以ParNew通常是运行在Server模式下的首选新生代垃圾收集器

    使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器

    Parallel Scavenge收集器
    Parallel Scavenge收集器依然是个采用复制算法的多线程新生代收集器,它与其他的收集器的不同之处在于它主要关心的是吞吐量,而其他的收集器关注的是尽可能的减少用户线程的等待时间(缩短Stop The World的时间)。吞吐量=用户线程执行时间/(用户线程执行时间+垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集花费时间1分钟,那么吞吐量就是 99%

    停顿时间越短适合需要和用户进行交互的程序,良好的响应能够提升用户的体验。而高效的吞吐量可以充分的利用CPU时间,尽快的完成计算任务,所以Parallel Scavenge收集器适用于后台计算型任务程序。

    -XX:MaxGCPauseMillis可以控制垃圾收集的最大暂停时间,需要注意不要以为把这个时间设置的很小就可以减少垃圾收集暂用的时间,这可能会导致发生频繁的GC,反而降低了吞吐量

    -XX:GCTimeRatio设置吞吐量大小,参数是取值范围0-100的整数,也就是垃圾收集占用的时间,默认是99,那么垃圾收集占用的最大时间 1%

    -XX:+UseAdaptiveSizePolicy 如果打开这个参数,就不需要用户手动的控制新生代大小,晋升老年代年龄等参数,JVM会开启GC自适应调节策略

    Serial Old收集器
    Serial Old收集器也是个单线程收集器,适用于老年代,使用的是标记-整理算法,可以配合Serial收集器在Client模式下使用。

    它可以作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。(后面CMS详细说明)

    Parallel Old收集器
    Parallel Old收集器可以配合Parallel Scavenge收集器一起使用达到“吞吐量优先”,它主要是针对老年代的收集器,使用的是标记-整理算法。在注重吞吐量的任务中可以优先考虑使用这个组合

    -XX:+UseParallelOldGc设置老年代使用该回收器。

    XX:+ParallelGCThreads设置垃圾收集时的线程数量。

    CMS收集器
    CMS收集器是一种以获取最短回收停顿时间为目标的收集器,在互联网网站、B/S架构的中常用的收集器就是CMS,因为系统停顿的时间最短,给用户带来较好的体验。

    -XX:+UseConcMarkSweepGC设置老年代使用该回收器。

    -XX:ConcGCThreads设置并发线程数量。

    CMS采用的是标记-清除算法,主要分为了4个步骤:

    • 初始化标记
    • 并发标记
    • 重新标记
    • 并发清除

    初始化标记和重新标记这两个步骤依然会发生Stop The World,初始化标记只是标记GC Root能够直接关联到的对象,速度较快,并发标记能够和用户线程并发执行;重新标记是为了修正在并发标记的过程中用户线程产生的垃圾,这个时间比初始化标记稍长,比并发标记短很多。整个过程请看下图面试官常问的垃圾回收器,这次全搞懂-开源基础软件社区

    优点

    CMS是一款优秀的收集器,它的主要优点:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

    缺点

    • CMS收集器对CPU资源非常敏感。 在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

    • 无法处理浮动垃圾。 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,回收阀值可以通过参数-XX:CMSInitiatingoccupancyFraction来设置;如果回收阀值设置的太大,在CMS运行期间如果分配大的对象找不到足够的空间就会出现“Concurrent Mode Failure”失败,这时候会临时启动SerialOld GC来重新进行老年代的收集,这样的话停顿的时间就会加长。

    • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大问题,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。为了解决这个问题CMS提供了一个参数-XX:+UseCMSCompactAtFullCollecion,如果启用,在Full GC的时候开启内存碎片整理合并过程,由于内存碎片整理的过程无法并行执行,所以停顿的时间会加长。考虑到每次FullGC都要进行内存碎片合并不是很合适,所以CMS又提供了另一个参数-XX:CMSFullGCsBeforeCompaction来控制执行多少次不带碎片整理的FullGC之后,来一次带碎片整理GC

    G1收集器
    G1是一款面向服务端应用的垃圾回收器。

    并行与并发:与CMS类似,充分里用多核CPU的优势,G1仍然可以不暂停用户线程执行垃圾收集工作
    分代收集:分代的概念依然在G1保留,当时它不需要和其他垃圾收集器配合使用,可以独立管理整个堆内存
    空间的整合:G1整体上采用的是标记-整理算法,从局部(Region)采用的是复制算法,这两种算法都意味着G1不需要进行内存碎片整理
    可预测的停顿:能够让用户指定在时间片段内,消耗在垃圾收集的时间不超过多长时间。
    Region
    虽然在G1中依然保留了新生代和老年代的概念,但是采用的是一种完全不同的方式来组织堆内存,它把整个堆内存分割成了很多大小相同的区域(Region),并且新生代和老年代在物理上也不是连续的内存区域,请看下图:

    面试官常问的垃圾回收器,这次全搞懂-开源基础软件社区

    每个Region被标记了E、S、O和H,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象,当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。Region区域的内存大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是2的幂次方,如:1M、2M、4M、8M

    G1的GC模式

    • 新生代GC:与其他新生代收集器类似,对象优先在eden region分配,如果eden region内存不足就会触发新生代的GC,把存活的对象安置在survivor region,或者晋升到old region
    • 混合GC:当越来越多的对象晋升到了old region,当老年代的内存使用率达到某个阈值就会触发混合GC,可以通过参数-XX:InitiatingHeapOccupancyPercent设置阈值百分比,此参数与CMS中-XX:CMSInitiatingoccupancyFraction的功能类似;混合GC会回收新生代和部分老年代内存,注意是部分老年代而不是全部老年代;G1会跟踪每个Region中的垃圾回收价值,在用户指定的垃圾收集时间内优先回收价值最大的region
    • Full GC:如果对象内存分配速度过快,混合GC还未回收完成,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,此过程与CMS类似,会导致异常长时间的暂停时间,尽可能的避免full gc.

G1 的优势是什么?https://blog.csdn.net/aa119101/article/details/125505600

G1:把堆划分成多个大小相等的Region,新生代和老年代不再物理隔离,多核 CPU 和大内存的场景下有很好的性能。新生代使用复制算法,老年代使用标记-压缩(整理)算法。

G1致力于在下面的应用和环境下寻找延迟和吞吐量的最佳平衡:

  • 堆大小达到10GB以上,并且一半以上的空间被存活的对象占用
  • 随着系统长期运行,对象分配和升级速率变化很快
  • 堆中存在大量内存碎片
  • 垃圾收集时停顿时间不能超过几百毫秒,避免垃圾收集造成的长时间停顿。

标签:收集器,对象,模型,线程,内存,JVM,方法,加载
From: https://www.cnblogs.com/luojw/p/17131411.html

相关文章

  • JVM中的GC系统
    什么是GC?GC(GarbageCollection)称之为垃圾回收,在JVM的执行引擎中自带这样的一个GC系统,此系统会按照一定的算法对内存进行监控和垃圾回收。如何判断哪些对象是垃圾?1、引用......
  • 通过 application 将需要存储内容放在内存中
    内存文件的创建创建一个类MainApplication继承于Application,该类要采用单例模式,如下:publicclassMainApplicationextendsApplication{privatefinalstat......
  • JVM初始化一个类包含几个步骤?
    加入这个类还没有被加载和连接,则程序先加载并连接该类加入该类的直接父类还没有初始化,则先初始化其直接父类加入类有初始化语句,则系统依次执行这些初始化语句当执行第......
  • redis的key过期策略+内存淘汰策略
    redis的key过期策略是怎么样的redis的过期策略主要是定期删除和懒删除来实现的定期删除redis定时每间隔大约100ms进行随机抽选择1批key,对过期的key进行删除,并且根......
  • 第一周复习笔记(MySQL、Redis、JVM、JUC)
    MySQL1.引擎1.1Innodb和MyIsAM的区别1.2Innodb的逻辑存储结构2.索引2.1索引的分类2.2索引优化2.3索引失效的场景3.事务3.1事务的隔离级别3.2ACID原则......
  • three.js 性能优化之模型转化与压缩
    模型转换obj转gltf 安装插件npmi-gobj2gltf执行转换命令obj2gltf-i11-6.obj-o11-6.gltf-u 模型压缩安装gltf-pipelinenpmi-gg......
  • rk3588使用NPU部署 wespeaker声纹模型
    1.wespeaker导出onnx模型时,需要使用静态模型导出。因为RKNN不支持动态推理。  wespeaker/bin/export_onnx.py修改如下,注释掉动态轴dynamic_axes  关于onxx静态和......
  • java 内存锁
    importlombok.extern.slf4j.Slf4j;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.locks.Lock;importjava.util.con......
  • 内存检测工具
                    ......
  • 谈JVM参数GC线程数ParallelGCThreads合理性设置
    作者:京东零售刘乐1.ParallelGCThreads参数含义在讲这个参数之前,先谈谈JVM垃圾回收(GC)算法的两个优化标的:吞吐量和停顿时长。JVM会使用特定的GC收集线程,当GC开始的时候,GC......