当前位置: 首页 > news >正文

JAVA基础-java虚拟机

  • JVM本质是一个运行在计算机上的应用程序,它隐藏了操作系统及硬件的复杂性,为java程序提供了一个统一、稳定、安全的运行平台
  • JVM的功能:
    • 解释和运行
    • 内存管理:自动为对象、方法的分配内存、自动垃圾回收
    • 即使编译
  • Java具有跨平台的特定,即一次运行到处运行,其核心就是JVM虚拟机
  • 在日常开发中,我们编写的是.java文件,编译器会将java文件编译为字节码文件,字节码文件具有平台无关性,运行时JVM将字节码文件加载到内存中,提供一定的内存管理,然后通过解释执行或者即时编译的方式运行java程序
    • 解释执行:一行一行的执行,随着程序的运行需要反复执行
    • 即时编译:将热点代码编译为机器码缓存起来,执行的时候直接执行缓存起来的机器码

常见的JVM版本

  • HotSport(Oracle JDK版):Oracle的,使用广泛,稳定可靠,JIT支持,场景:默认
  • HotSport(Open JDK版):Oracle的,使用广泛,稳定可靠,JIT支持,开源,场景:默认或者对JDK有二次开发需求
  • GraaJVM:Oracle的,多语言支持,高性能,JIT,AOT,场景:微服务、云原生架构、需要多语言混合编程
  • DragonwellJDK 龙井:阿里巴巴的,基于OpenJD K的增强,高性能,bug修复,安全性提升,一些特性,场景:电商、物流、金融领域对性能要求比较高
  • Eclipse OpenJ9:IBM,高性能、可扩展,JIT,AOT,场景:微服务、云原生架构 

Java虚拟机的组成

  • 类加载器ClassLoader:将不同数据源的字节码文件加载到内存中
  • 运行时数据区:提供内存的管理功能,将内存为方法区,堆,栈,本地方法栈,程序计数器,用于存储加载到内存中的字节码信息,以及程序运行时对象的常见和销毁
  • 执行引擎:执行引擎通过和运行时后数据区交互,通过解释执行或者即时编译的方式运行程序,以及垃圾回收
  • 本地接口:其他语言开发的接口

小结:

  • JVM到底是什么?
    • JVM本质上是运行在计算机上应用程序,它隐藏操作系统以及硬件的复杂性,为应用程序提供统一、稳定、安全的运行平台
  • JVM的三大核心功能?
    • 解释和执行
    • 自动内存管理
    • 即使编译
  • 常见的JVM虚拟机有哪些?
    • Oracle的HotSport虚拟机:使用最广泛
    • Oracle的GraaVM虚拟机:在HotSport的基础上做了增强,主要提高微服务或者云原生架构场景下的系统性能
    • 阿里的龙井JDK:在OpenJDk基础上做的二次开发,主要是为了应对电商、物流、金融等对性能要求极高的场景

字节码文件详解

字节码文件的组成

  • 为什么学习字节码文件的组成?
    • 从字节层面解释代码的执行原理
    • 解决工作中遇到的实际问题,比如程序报错,系统升级问题
  • Jclasslib字节码查看工具
  • 字节码文件的组成:
    • 基础信息:魔数、字节码文件对应的java版本号、访问表示符、父类和接口等
    • 常量池:保存字符串常量、类或接口名、字段名,主要在字节码文件中使用
    • 字段:当前类或接口声明的字段信息
    • 方法:当前类或接口声明的方法信息字节码指令
    • 属性:类的属性,比如源码的文件名,内部类的列表等
  • i=i++:在字节码指令中,先load再++然后store,而++i是先++在load再store
  • 字节码常用工具:
    • javap -v
    • jclasslib
    • arthas 

类的生命周期

  • 类的生命周期的应用场景:
    • 运行时常量池
    • 多态的原理
    • 类加载器的作用
    • 类的解密和解密
  • 类的生命周期:载入、连接、初始化、使用、卸载
  • loading载入阶段:
    • 类加载器根据类的全限定类名通过不同渠道以二进制流的方式获取字节码信息
    • 类加载完成之后,JVM会将字节码中的信息保存到方法区中,生成instanceKlass对象,保存类的所有信息,里面还包含实现特定功能比如多态信息
    • 还会再堆中生成同InstanceKlass对象类似的CLass对象,作用是在java代码中去获取类的信息以及(JDK8之后)存储静态字段的数据,InstanceKlass对象和Class对象之间相互关联,可以相互找到对方
    • 为什么要在方法区和堆中分别创建两个对象?
      • InstanceKlass对象是由C++编写的对象,代码中无法直接获取,有部分信息不是提供给开发者
      • Class对象是java语言包装之后的对象,是提供给开发者使用的对象,Class对象中的字段是少于InstanceKlass对象的
  • 连接阶段:
    • 验证:校验字节码中的内容是否符合java虚拟机规范
    • 准备:为静态变量分配内存,并且初始化变量,如果被static修饰初始为默认值,如果被static final修直接赋值为代码给定的值
    • 解析:将常量池中的符号引用替换为直接引用
      • 符号引用就是在字节码文件中使用编号来访问常量池中的内容
  • 初始化阶段:
    • JDK8:-XX:+TraceClassLoading JDK9即以上:-Xlog:class+load=info 参数可以打印出加载并初始化的类
    • 执行静态代码块中的代码
    • 为静态变量赋值
    • 以下几种方式会导致类的初始化:
      • 访问一个类的静态变量或者静态方法,但是访问被final修饰的常量不会触发初始化
      • 调用Class.forName(String className)会触发类的初始化
      • new 创建一个对象会触发初始化
      • 执行main方法的类会触发初始化
    • 以下几种方式不会导致类的初始化:
      • 无静态变量的赋值且 无静态代码块
      • 有静态变量但无静态变量的赋值
      • 静态变量被final修饰,但是被final修饰的静态变量需要执行指令才能得出结果,则会执行初始化
      • 创建数组不会导致数组中的元素的类初始化
    • 如果直接访问父类的静态变量,子类的初始化不会被执行。如果直接子类初始化之前,会先触发父类的初始化,然后是子类的初始化
class A02{static  int a = 0;static {a = 1;}
}class B02 extends A02{static {a = 2;}
}//直接访问父类的静态变量,不会触发子类的初始化
public static void main(String[] args) {System.out.println(B02.a);//1
}
//子类在初始的时候,会先调用父类的初始化
public static void main(String[] args) {new B02();System.out.println(B02.a);//2
}

类加载器

  • 类加载器是在类的加载阶段获取不同渠道的二进制流转化为字节码的功能
  • 是java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术【允许自定义】
  • 类加载器的应用场景:
    • 企业级应用
      • SPI机制
      • 类的热部署
      • Tomcat类的分离
    • 解决线程问题:
      • 使用Arthas不停机解决线上故障
    • 面试题:
      • 双亲委派机制
      • 打破双亲委派
      • 自定义类加载器

类加载器的分类

  • 分为两类:
    • 虚拟机底层实现:HotSport 使用C++实现,用于加载程序运行时的基础类,比如String等,确保可靠性
    • Java代码实现:JDK有默认提供的或者自定义,都继承自CLassLoader类
JDK8之前的类加载器:
  • 虚拟机默认的类加载器:
    • 启动器类加载器:加载Java最核心的类
  • Java中:
    • 扩展类加载器:通用类加载器
    • 应用类加载器:加载应用使用的类,以及第三方jar包
  • 启动类加载器:
    • 是由HotSport提供使用C++实现的用于加载Java核心类的类加载器,加载jar/lib目录下的类文件
    • 使用启动参数扩展使用启动类加载器加载用户jar包:-Xbootclasspath/a:jar目录/jar包名
  • Java中默认的类加载器
    • 都是JDK提供使用Java编写的类加载器,位于sun.misc.Launcher中,是一个静态内部类。继承自URLCLassLoader,具体通过目录或者指定jar包字节码文件加载到内存
    • 扩展类加载器:
      • 默认加载jre/lib/ext下的类文件
      • 使用启动参数扩展使用扩展类加载器:-Djava.ext.dirs=jar包目录,这种方式会覆盖默认目录,需要使用分隔符默认目录:扩展目录
    • 应用程序类加载器:
      • 加载用户自定义的类和第三方jar包
      • 应用程序类加载器也会加载启动类加载器和扩展类加载器加载的内容,涉及到双亲委派机制
JDK9之后的类加载器
  • JDK9引用了module的改变,原本从jar包中加载类变为从jmod中加载类
  • BuiltinClassLoader实现从模块中找打要加载的字节码资源文件
  • 启动类加载器:
    • 使用Java编写,位于jdk.internal.loader.ClassLoaders类中,BootClassLoader继承自BuiltinClassLoader,但是仍然不能被java代码获取
  • 扩展类加载器:
    • 被替换为平台类加载器PlatformClassLoader,也继承自BuiltinClassLoader
    • 平台类加载器的存在更多是兼容老版本的设计方案,自身没什么特殊的逻辑
  • 应用程序类加载器:
    • 从URLClassLoader替换为BuiltinClassLoader,其他没什么区别

双亲委派机制

  • 双亲委派机制的核心是解决在多个类加载器的情况下一个类到底有谁加载的问题
  • 双亲委派机制的功能:
    • 保证类加载的安全性:通过双亲委派机制避免恶意代码替换JDK的核心类库,比如String类,确保核心类的完成和安全
    • 避免重复加载:可以避免同一个类被多次加载
  • 双亲委派机制指的是:当一个类加载器接受到加载类的任务是,会自底向上查找是否加载过,在有顶向下进行加载 
    • 应用类加载器->扩展类加载器->启动类加载器:自底向上查找是否加载
    • 启动类加载器->扩展类加载器->应用类加载器:自顶向下的尝试加载
  • 如何使用代码的方法主动加载一个类?
    • 使用Class.forName方法,会使用当前类的类加载器去加载指定的类
    • 获取到类加载器,通过类加载器的loadClass方法指定某个类的类加载器加载
  • ClassLoader的双亲委派机制的源码分析:
    • loadClass方法:类加载的入口,提供了双亲委派机制。内部会调用findClass
    • findClass方法:由类加载器子类实现,获取二进制数据数据调用defineClas,比如URLClassLoader会更具文件路径获取文件中的二进制数据
    • defineClass方法:做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
    • resolveClass:执行类生命周期中连接阶段

打破双亲委派机制

  • 打破双亲委派机制和方法:重写loadclass方法
  • 什么情况需要打破双亲委派机制:
    • 全限定类名相同,但是代码不同。比如不同容器中的类,Tomcat
  • 自定义类加载器【重写loadclass方法,打破】:
    • 自定义类加载器并重写loadClass方法,就可以将双亲委派机制的代码去除
    • Tomcat通过这种方式实现应用之间类隔离,为不同应用创建自己类加载器
    • 自定义类加载器如果不手动设置parent,默认的父类加载器为AppClassloader
  • 线程上下文类加载器【没有打破】:
    • 利用上下文类加载器加载类,比如JDBC和JNDI等
    • SPI机制:JDK内置的一种服务发现机制
    • 通过获取当前线程的类加载器获取应用程序类加载器
  • Osgi框架的类加载器【了解即可】
    • Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载
  • 问题:两个自定义类加载器加载相同的限定名类,不会冲突吗?
    • 不会,在java虚拟机中只有使用相同的类加载器+相同的限定名,才会被任务是同一个类

小结

  • 类加载器的作用:
    • 虚拟机调用类加载器将不同渠道的二级制流转化为字节数组,然后调用虚拟机底层方法使用字节数组在方法去和堆区分别常见InstenceKlass对象和class对象
  • 有几种常见的类加载器:
    • 启动类加载器:用于加载java的核心类库,比如jre/lib下的包
    • 扩展类加载器(平台类加载器):用于加载Java扩展类库,比如jre/lib/ext下的包
    • 应用程序类加载器:程序员开发的类或者第三方类库的包
    • JDK8之前类加载器是加载jar包,JDK9之后类加载器加载的是jmod,所以在类加载器的实现上也有所不同,启动类加载器在JDK8时有C++实现,在JDK就由JAVA实现的BootClassloader继承自BuitinClassLoader,扩展类加载器在在JDK8时ExtClassloader,继承在UrlClassloader,在9则是PlantFormClassloader,继承自BuitinClassloader,应用程序类加载器:JDK8时AppClassloader,继承自UrlClassloader,替换为继承BuitinClassLoader
  • 什么是双亲委派机制?
    • 核心是为了解决在有多个加载器的情况下,避免重复加载类
    • 方法:自底向上的检查类是否被加载,自顶向下的判断类是否能被加载
    • 功能:保证核心类的完整和安全性,避免类的重复夹杂
  • 怎么打破双亲委派机制?
    • 为什么要打破双亲委派机制?
      • 因为在某些场景中,会存在相同的限定名但是实现并不相同的情况,如果不打破双亲委派机制,则相同限定名的类不会在再次加载
    • 双亲委派机制的核心是loadClass方法,只要重写loadClass方法,去除委派逻辑即可
      • 使用重写loadClass方法的自定义类加载器:注意自定义类加载器默认的父加载为AppClassloader,可以手动重写构造器,重新指定
      • 使用线程上下文类加载器:比如JDBC,实际上是本应由启动类加载器加载的驱动,在初始化阶段调用线程上下文类加载器去加载(默认情况下是应用程序类加载器),实际上并没重写loadClass方法,所以也可以不打破双亲委派
  • 类加载器的核心方法:
    • loadClass方法:类加载器的入口,实现双亲委派,内部调用findClass方法
    • findClass方法:需要由子类实现,主要是获取不同渠道的二进制流然后调用defineClass方法,比如UrlFindClass方法就是根据具体路径获取文件的二进制数据
    • defineClass方法:做一些类名校验,然后调用虚拟机底层接口,在内存中创建InstenctKlass和class对象
    • resovleClass方法:进行类声明周期的连接阶段

运行时数据区

  • 负责管理JVM使用到的内存,比如创建和销毁对象
  • 分为:
    • 线程私有的:程序计数器、栈、本地方法栈
    • 线程共享的:方法区、堆

内存调优学习路径

  • 了解运行时内存结构:了解JVM运行过程中每一部分的内存结构以及哪些部分容易出现内存溢出
  • 掌握内存问题的产生原因:学习代码中常见的几种内存泄漏,性能问题的常见原因
  • 掌握内存调优的基本方法:学习内存泄漏、性能问题等常见JVM问题的常规解决方案

程序计数器

  • 属于线程私有,用于存放程序下一步要执行的字节码指令的地址
  • 程序员不需要控制程序计数器
  • 不会发生内存溢出问题

  • 栈:线程私有,用于保存运行时的方法信息
  • 采用栈的数据结构,来管理方法调用的基本数据,先进后出,每个方法的调用使用一个栈帧来保存
  • 在HotSport虚拟机中,使用了同一个栈空间来保存
Java虚拟机栈
  • 存放的是Java编写的方法的栈帧
  • 栈帧的组成:
    • 局部变量表:是在运行过程中存放所有的局部变量
    • 操作数栈:是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
    • 帧数据:主要包含动态链接「方法中其他类的属性或者方法」、方法出口、异常表的引用
  • 栈的内存溢出:
    • 占用内存超过栈内存可分配的最大内存,就会内存溢出
    • 虚拟机就会抛出栈内存溢出StackOverflowError的错误哦
本地方法栈
  • 存放的是使用C++编写的native本地方法的栈帧

  • 线程共享,创建出来的对象都会分配在堆内存中,class对象以及静态变量也在堆中
  • 栈中的局部变量表中,可以存放堆内存中的引用
  • 堆的内存溢出:会抛出OutofMermoryError错误
  • 堆空间中需要关注的三个值:
    • used:已经使用的内存空间
    • total:可以使用的内存空间,默认是系统内存的1/64
    • max:最大分配的内存空间:
      • 默认是系统内存的1/4
      • 如果total比max小,则used到达total时,还可以继续存放,不会抛出oom
      • 注意:并不是used=total=max才会出现oom,这个和垃圾回收器有关
    • 一般情况下,将total和max设置为相同值,避免申请和收缩的时间

方法区

  • 线程共享,主要包含类的元信息、运行时常量池、字符串常量池
  • 类的元信息:一般称之为InstanceKlass对象,在类加载阶段完成,包含基本信息、常量池、字段、方法、属性等
  • 运行时常量池:在字节码文件中的常量池称为静态常量池,在加载+连接阶段完成之后转化为运行时常量池
  • 字符串常量池: 存储代码中定义的常量字符串
  • JDK7之前中:方法区是放在堆中的永久代中,堆的大小由虚拟机参数来控制
  • JDK8之后:放堆之外的元空间中,位于操作系统维护的直接内存,默认只要不超过操作系统上限,可以一直分配
  • 在JDK8之后,方法区基本不会发生内存溢出

直接内存

  • 不属于JVM运行时数据区,是由操作系统直接管理的内存区域,JDK8之后的元空间就在这块区域中
  • NIO:同步非阻塞IO模型,也使用了直接内存,为了解决一下两个问题:
    • 堆中的对象如果不在使用被回收,回收时会影响对象的创建和使用
    • 提高读写文件文件的效率,正常需要将文件读取到直接内存,然后复制到堆中,现在直接仅读取到直接内存,在堆中维护直接内存的引用,较少数据复制的开销,使用ByteBuffer.allocateDirect()方法常见直接直接缓冲区,还有内存映射文件MappedButeBuffer可以将文件直接映射到内存,
  • 会存在内存溢出问题:JVM在使用直接内存的时候也会限制大小

小结

  • 运行时数据区分成那几部分,每一部分的作用?
    • 线程私有:
      • 程序计数器:用于存储程序下一步要执行的字节码指令行号
      • 栈:使用栈的数据接口,LIFO的方法存储方法运行时栈帧,方法执行,栈帧入栈,结束栈帧出栈
        • java虚拟机栈:存储java方法的栈帧
        • 本地方法栈:存储native方法栈帧
        • 栈帧包含:局部变量表、操作数栈、帧数据:动态链接「从符号引用转为直接引用」、方法出入口、异常表
        • 会发生内存溢出,抛出stackoverflorwerror,栈的内存溢出基本都是递归造成,可以通过JVM参数调整栈内存
    • 线程共享:
      • 方法区:是一个抽象的概念,不同的JDK版本有不同的实现,用于存储类的元信息、运行时常量池、字符串常量池
        • 类的元信息:指的是在类的加载阶段创建的instanceKlass对象:包含基本信息、常量池、方法、字段、属性等
        • 运行时常量池:相对静态常量池,在字节码文件中使用编号维护的常量池称为静态常量池,在加载和链接完成后,转化为运行时常量池
        • 字符串常量池:用于存储字符串常量,JDK8之前,字符串的拼接使用StringBuilder,会主动在字符串常量池常见堆的引用,使用intern方法返回的引用和拼接之后的引用是相同的。JDK9之后,JVM对字符串拼接进行了优化,不在使用StringBuilder,而是使用invokeDymaic方式,不会主动在常量池创建堆的引用
        • 在JDK7之前方法区时在堆中的永久代,在JDK8之后,方法区的实现叫元空间,在直接内存中,独立与运行时数据区,由操作系统控制
        • 方法区基本不会发生内存,如果发生就是抛出outofMermoryError
      • 堆:创建的对象在堆中分配内存,类加载阶段完成之后创建的class对象以及静态变量也在堆中
        • 核心的三个变量:
          • used:使用的堆内存
          • total:能够使用的堆内存
          • max:堆内存的上限,total会随着used的增加而增加,但不会大于max
        • 堆会发生内存溢出,抛出outogmermoryerror

自动垃圾回收

  • 垃圾回收器在执行引擎中
  • C/C++采用手动垃圾回收的方式:
    • 优点:回收及时,有开发者把控时机
    • 缺点:编写不正确,然后造成空指针、内存泄漏等问题
  • JAVA采用自动垃圾回收的方法:
    • 优点:无需开发者关心,降低编程难度,降低bug的风险
    • 缺点:开发者无法控制内存回收的时机
  • 应用场景:性能优化:对垃圾回收器进行合理的设置能够有效提升程序的执行能力 
  • System.gc()可以手动触发垃圾回收,和start方法类似,仅仅只向JVM发送垃圾回收请求,具体是否要执行由JVM判断
  • VM参数控制台打印垃圾回收信息:-verbose:gc

方法区的回收

  • 类生命周期中类的卸载:指的就是方法区中的类是如何被回收的
  • 判断一个类可以被卸载,需要同时满足以下三个条件:
    • 类的所有实例对象都已经回收,在堆中不存在任何该类的实例对象以及子对象
    • 加载该类的类加载器已经被回收
    • 该类的class对象没有任何地方被引用
  • 绝大部分的类都是被应用程序类加载器加载的,而应用程序类加载器在运行中时不会被回收的,所以我们开发的类只要被加载就不会被回收
  • 应用场景:在支持热部署的场景中:如JSP
    • 每个JSP文件对应一个唯一的类加载器,当一个JSP文件修改了,就直接卸载这个jsp类加载器,包括实例对象以及class对象也都会被回收,然后重新创建类加载器,重新加载JSP文件

堆的回收

  • 如何判断对象是否能被回收:主要是根据对象是否被引用来决定
  • 引用计数法:为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
    • 缺点:循环依赖的情况下,导致对象无法始终被引用,而无法回收
  • 可达性分析算法[java]:
    • 将对象分为两类:
      • 根对象:称为GC ROOT对象,java虚拟机中,根对象一般是不能被回收的,java虚拟机会保存GC ROOT表
      • 普通对象:从GCROOT对象之后的引用链中的所有对象都称为普通对象
    • 可达性分析算法指:如果某个普通对象能够通过GC ROOT的引用链找到,则不能被回收,反之可以被回收
    • 哪些对象可以被称为GC ROOT对象:
      • 线程Thread对象:
        • 线程对象中会包含当前线程的栈空间的引用
      • 系统类加载器加载的class对象:
        • JDK8之前系统类加载器是指sun.misc.Launcher创建的类加载器
        • JDK9之后系统类加载器的是指java.internal.loader.ClassLoaders创建的类加载器
        • 通过系统类加载器加载class对象被称为GC ROOT,而class对象所持有的静态变量的引用也是永久可达的
      • 监视器对象:监视器对象持有的锁对象是不可被回收的
      • 本地方法调用时使用的全局对象
    • 如何查看:GC ROOT:通过arthas和MAT工具
五种对象引用
  • 强引用:可达性算法描述的对象应用称为强应用
  • 软引用:
    • 当程序内存不足时,就会将软引用的数据进行回收
    • JDK2之后提供了SoftReference类来实现软引用
    • 软引用常应用于缓存
 /*** 当程序内存不足时,会 先进行垃圾回收* 回收完成之后还不足,则会将SofrReference软引用包含的对象回收掉* 如果回收之后内存还是不足,则会抛出OOM*/
byte[] bytes = new byte[1024 * 1024];
//用于存放被释放掉的软引用对象
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
SoftReference<byte[]> softReference = new SoftReference<>(bytes,referenceQueue);
  • 弱引用
    • 弱引用包含的对象,不管内存是否足够,都会被回收
    • JDK2之后提供了WeakReference类来实现弱引用
    • 主要在ThreadLoacl中使用
    • 弱引用对象本身也可以使用引用队列进行回收
  • 虚引用
    • 虚引用称为幽灵引用,不能通过虚引用对象获取包含的对象
    • 虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知
    • 使用PhantomReference实现了虚引用
  • 终结器引用
    • 指的是在对象需要被回收时,对象会被放在Finalizer类中引用队列中,并在稍后由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法
垃圾回收算法
  • 垃圾回收主要做两件事:
    • 找到内存中存活的对象
    • 释放不在存活的对象的内存空间
  • 在java中垃圾回收的阶段是通过单独的GC线程来执行,不管是什么算法,都会有部分的时间会停止所有的用户线程。这个过程被称为STW,STW时间过长则会影响用户的使用
  • 如何判断GC算法是否优秀:
    • 吞吐量:指CPU用于执行用户代码的时间与CPU总执行时间的比值
    • 最大暂停时间:STW的最大值
    • 堆使用效率:比如标记清除算法可以使用整个堆空间,而复制算法只能使用一般的堆空间
    • 垃圾回收算法没有最好最还,只有适合不适合,不同场景应用不同的垃圾回收算法
  • 常见的垃圾回收算法:
    • 标记-清除算法:
      • 分为两个阶段:
        • 标记阶段:通过可达性算法,从GC ROOT开始通过引用链变量出所有存活的对象
        • 清除阶段:从内存中删除没有被标记的非存活对象
      • 优点:实现简单,第一阶段修改对象标记为,第二阶段删除对象
      • 缺点:
        • 碎片化问题:会导致内存出现很多细小的空间,如果再有大空间需求时而导致无法分配
        • 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,每次需要遍历链表来找合适的内存空间
    • 复制算法:
      • 将堆内存分为两块:From和To,每次对象分配只能使用From空间,在GC阶段,将From空间中存活的对象复制到To空间中,然后替换空间名称
      • 优点:
        • 吞吐量高
        • 不会发生碎片化
      • 缺点:
        • 内存利用率不高
        • 效率不如标记清除算法
    • 标记-整理算法:
      • 标记整理算法也称标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案
      • 第一阶段:标记阶段:对所有存活对象进行标记,可达性算法
      • 第二阶段:整理阶段:将存活的对象移动到堆的一端。清理掉剩余的空间
      • 优点:
        • 内存使用率高
        • 不会发生碎片化
      • 缺点:整理阶段效率不高,有很多算法对整理阶段进行优化
    • 分代GC:
      • 会将标记清除、复制、标记整理算法组合惊进行使用
      • 分代GC将堆划分为两块:
        • 年轻代:存放存活时间比较短的对象
          • Eden区:
          • 幸存者区:
        • 老年代:如果存放时间比较长,则会从年轻代搬到老年代
      • 分代GC时,创建的对象,首先会被方法Eden区,随着Eden的对象越来越多,如果Eden满了,就会出发一次年轻代的GC,此时会把eden区和from区需要回收的对象回收,把没有回收的独享放入To区,每次minor GC之后,对象的年龄加1,当年龄到达阈值最大是15,对象就会被晋升至老年代,
      • 还有一种:如果minor GC之后,年轻代空间还是不足,此时即便有的对象没有到达阈值,也会被放入老年代
      • 当老年代空间不足时,会先出发minorGC,如果还是不足,就会触发FullGC对整个堆进行回收
      • 为什么分代GC算法要把堆分为年轻代和老年代?
        • 系统中的绝大部分对象创建出来之后很快就不在使用可以直接被回收
        • 但是有一些长期存活的对象,比如Spring的大部分bean对象
        • JVM默认年轻代远小于老年代
        • 方便调整不同空间的大小,应对不同应用场景下的内存需求
        • 使用不同的回收算法:新生代使用复制算法,老年代使用标记清除或标记整理算法
        • 分代设计,允许只发生minor GC就能满足内存需求,则无需对整个堆进行Full GC,大大减少STW时间
垃圾回收器
  • 垃圾回收器是垃圾回收算法的具体实现
  • G1垃圾回收器可以控制新生代也可以控制老年代
  • 其他的垃圾回收器的组合:
    • Serial:Serial Old
    • ParNew:CMS[JDK14废弃了CMS]
    • Parallel Scavenge:Parallel Old
  • 垃圾回收器的分类:
    • Serial:Serial Old
      • Serial:应用在新生代,使用复制算法;
      • Serial Old:应用在老年代 使用标记整理算法,
      • 都是通过单线程进行GC,适用场景:硬件配置有限的场景
      • 优点:单核CPU吞吐量出色
      • 缺点:多核不如其他CPU
    • ParNew:CMS[JDK14废弃了CMS]
      • ParNew:应用在新生代,复制算法,是对serial的优化,使用了多线程;
        • 优点:多线程STW时间更短
        • 缺点:吞吐量和停顿时间不如G1,JDK9之后不建议使用
      • CMS:应用在老年代,标记清除算法,关注系统的暂停时间,允许用户线程和GC线程在某些步骤中,同时执行,减少STW时间,
        • 执行步骤:初始标记->并发标记->重新标记->并发清理
        • 优点:用户线程暂停时间短,用户体验感好
        • 缺点:内存碎片、退化问题、浮动垃圾,在JDK14被废弃
        • 适用场景:适合大型互联网用于请求数据量大,频率高的场景
    • Parallel Scavenge:Parallel Old
      • PS:JDK8默认的年轻代复制算法垃圾回收器,多线程GC,关注系统吞吐量,具备自动调整堆内存大小的特点
        • 优点:吞吐量高,能自动或在手动调整堆参数
        • 缺点:不能保证单词的停顿时间
        • 适用场景:后台任务,不需要与用于交互,并确容易产出大量的对象,比如大数据处理,大文件导出
      • PO:是为PS设计的老年代版本,多线程GC,标记整理算法
        • 优点:并发收集,多核CPU效率高
        • 缺点:暂停时间会比较长
        • 适用场景:与ps配套使用
    • G1垃圾回收器:JDK9之后默认
      • G1的设计目标是将PS和CMS两个垃圾回收器的优点融合
      • 支持巨大的堆空间回收,并有较高的吞吐量
      • 支持多CPU并行GC
      • 允许用户设置最大暂停时间
      • G1将整个堆划分为大小相等的区域:Eden、Survivor、odl、Humongous
      • 垃圾回收的两种方法:
        • 新生代回收:
          • 采用复制算法回收eden和survivo去中不用的对象,会导致STW
          • 可以通过-XX:MaxGCPauseMillis=n(默认200)参数设置最大暂停时间
        • 混合回收:
          • 当总堆占用率到达阈值默认45%-XX:InitiatingHeapOccupancyPercent=n
          • 回收Eden、Survivor以及部分odl、Humongous的区域,采用复制算法
          • 执行流程:
            • 初始标记:多线程标记GCROOT对象及其关联独享,暂停用户线程
            • 并发标记:标记引用链上剩余的对象,不会暂停用户线程
            • 最终标记:标记漏标或错标对象,不管上一步新建的对象,暂定用户线程
            • 并发复制清理:优先清理存活度最低的块
            • 当整个堆空间都满了,没有空闲区域用于复制算法的回收,则出发FullGC

小结

  • java中有哪几块内存需要进行垃圾回收?
    • 线程私有的内存空间不需要回收,应用在线程结束会主动释放
    • 需要进行垃圾回收的主要就是方法区和堆
      • 方法区中存储的时候类的类的云信息,运行时常量池,以及字符串常量池,所以方法区的内存回收就涉及到类的卸载,判断类能否被卸载:第一:类的实力对象都已经被回收第二类的class对象没有被引用第三加载该类的类加载器已经被回收需要同时满足以上三点类才能被卸载,一般情况下,我们创建的类都是由应用系统类加载器加载,但是应用系统类加载器不会被回收,所以类也不会被卸载,方法区的内存回收一般应用在热部署的场景,比如JSP,当JSP文件被修改,就会加载该类的类加载器回收,然后重新创建新的类加载器重新记载
      • 堆中存储的是创建的对象实例以及class对象和静态常量,判断对象是否能被回收是看对象是否被引用,有两种方法:引用计数法:通过维护计数器来实现,但是当出现循环依赖,则会导致对象始终无法被回收,还有就是可达性分析算法:将对象分为GCroot对象和普通对象,如果对象能够通过GCroot的引用链找到,则不会被回收,反之则表示对象可以被回收
        • GCROOT:Thread对象、监视器对象、系统类加载器加载的class对象、本地方法使用的全局变量都是GCroot对象
  • 有哪几种常见的引用类型
    • 强引用:在GCroot引用链中的对象的引用称为强引用,不会被回收
    • 软引用:如果内存不足时,就回收软引用对象,不论对象是否被引用,一般用于缓存,通过SoftRefence对象包装
    • 弱引用:只要发生垃圾回收,弱引用就会被回收,比如ThreadLocalMap的key,被weakReference包装
    • 虚引用:称为幽灵引用,无法通过引用找到对象,唯一的用处就是在发生垃圾回收时会发送对应的通知
    • 终结器引用:根Finalizer类有关,会调用finalize方法进行垃圾回收,但是回收的时机不确定
  • 有哪几种常见的垃圾回收算法?
    • 标记清除算法:
      • 分为两个阶段:标记阶段:通过可达性分析算法将标记为可回收和不可回收。清除阶段:清理不可回收的对象
      • 优点:实现简单
      • 缺点:内存碎片化、分配速度慢,因为内存碎片化,所以需要维护一个可用空间链表,每次分配需要遍历链表找到合适的空间
    • 复制算法:
      • 将堆空间分为Form去和To区,新建的对象方法Form区,当Form区空间不足时,通过可达性算法找出活跃对象,然后复制到To,然后清理Form区,将让区域名称替换
      • 优点:吞吐量高,不会发生碎片化
      • 缺点:效率不如标记清除,空间利用率低
    • 标记整理算法:
      • 是在标记清除算法之上针对内存碎片化的优化
      • 也分为两个阶段:标记阶段可达性算法标记活跃对象,整理阶段将活跃对象复制到堆空间的一端清理剩余空间
      • 优点:不会出现碎片化、内存利用率高
      • 缺点:整理阶段效率不高,但是有很多优化算法
    • 分代GC:
      • 是整合了上面的几种算法
      • 将内存分为
        • 新生代:用于存放存活时间较短的对象
          • endn区:创建的对象会分配在endn区
          • survivor区:用于寸法endn区存活下来的对象
        • 老年代:当对象的年龄到达一定阈值,就是进入老年代,所以老年代存放存活时间较长的对象
        • 新建对象会放在endn区,当endn区空间不足会触出发新生代GC,将存活对象放入survivor区采用的是复制算法,每次新生代GC,存活下来的对象年龄会加一,当到达一定阈值就会被放入老年代,阈值最大为15,不同的垃圾回收器阈值不同,对象进入老年代还有一种方式就是end区和servivor区空间都不足,也会讲部分没有到达阈值对象放入老年代
        • 当老年代的空间不足时,会先出发新生代GC,如果空间还不足,则会触发FullGC,对整个堆进行回收,采用标记清除或者标记整理不同的垃圾回收器实现不同
      • 为什么讲堆空间分为新生代和老年代:
        • 衡量一个GC算法的指标是:吞吐量、最大暂停时间、堆的使用效率,但是三者不可兼得
        • 我们大部分创建的对象生命周期都比较短,会被存在在新生代,由于新生代空间远小于老年代,使用复制算法保证吞吐量的的同时有STW也不会很长
        • 还有就是分代处理,如果新生代GC如果能够满足要求,则不需整个堆的GC
        • 并且可以通过参数配置新生代和老年代的空间大小,以满足不同场景下的内存需求
  • 常见的垃圾回收器有哪些?
    • JDK8默认的ps和pd组合的垃圾回收器
      • ps:新生代垃圾回收器,多线程并行GC,采用复制算法,关注吞吐量,具有能够自动调整堆空间大小的特点
      • po:老年代垃圾回收器,是为配合ps设置老年代垃圾回收器,采用标记整理算法
    • JDK9之后默认的G1垃圾回收器,多线程并发GC,新生代和老年代都可以空间,使用复制算法
      • G1将堆空间分配大小相同的块,然后在块中分配:endn、survivor、old、humgous
      • 两个阶段:
        • 新生代GC:通过复制算法回收endn和servivor区,同样通过年龄判断是否进入old或者占用大小进入humonous
        • 混合GC:当堆内存占用率到达一定阈值,就会触发混合GC默认是45%,会堆endn和servivor区一级部分old和humongus区进行回收
http://www.dtcms.com/a/343881.html

相关文章:

  • uniapp googlepay支付 内购项目
  • 豆包AI PPT与秒出PPT对比评测:谁更适合你?
  • 计算机毕设选题推荐 基于Spark的家庭能源消耗智能分析与可视化系统 基于机器学习的家庭能源消耗预测与可视化系统源码
  • Python办公之Excel(openpyxl)、PPT(python-pptx)、Word(python-docx)
  • 2026年计算机毕设推荐:基于大数据的慢性肾病数据可视化分析系统技术选型指南【Hadoop、spark、python】
  • 使用PPT进行科研绘图过程中常用的快捷键
  • 日志logging学习(1)
  • Gemini 2.5 Flash-Lite与 DeepSeek-V3 深度对比:谁在性价比上更胜一筹?
  • 【typenum】 21 类型级别计算最大公约数(Gcd)
  • map和set的使⽤
  • 52 C++ 现代C++编程艺术1-禁止隐式转换关键字explicit
  • Qt中用于图像缩放的核⼼⽅法QPixmap::scaled
  • 编写Linux下设备驱动时两种方案:内核态驱动开发和用户态驱动开发
  • --- 使用OpenFeign来优雅的对服务进行调用 ---
  • vue2怎么修改el-table样式
  • 金融风控AI引擎:实时反欺诈系统的架构设计与实现
  • CTFSHOW | 其他篇题解(二)web417 - web437
  • 进程间通信-IPC机制
  • 【开发日记】SpringBoot 实现支持多个微信小程序的登录
  • 初始数据结构——反射、枚举与Lambda的奇幻冒险
  • 如何理解AP服务发现协议中“如果某项服务需要在多个网络接口上提供,则应为每个网络接口使用一个独立的服务器服务实例。”?
  • 《Linux 网络编程一:网络编程导论及UDP 服务器的创建与数据接收》
  • “我 / 店模式” 靠联盟 + 积分破局,实现三方共赢!
  • 【Oracle】内存管理实战指南:ASMM vs AMM 配置全解析
  • Rust Web开发指南 第一章
  • 服务发现实例和服务实例是不同的
  • 血管介入医疗AI发展最新方向与编程变革:从外周、神经到冠脉的全面解析
  • RabbitMQ面试精讲 Day 27:常见故障排查与分析
  • yggjs_rlayout使用教程 v0.1.0
  • Linux系统之Ubuntu安装cockpit管理工具