JVM 内存管理常见面试问题

一、什么是JVM

Java Virtual Machine(Java虚拟机)是java程序实现跨平台的⼀个重要的⼯具(部件)。

HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是⽬前使⽤范围最⼴的Java虚拟机。

只要装有JVM的平台,都可以运⾏java程序。那么Java程序在JVM上是怎么被运⾏的?

通过介绍以下JVM的三个组成部分,就可以了解到JVM内部的⼯作机制

  • 类加载系统:负责完成类的加载
  • 运⾏时数据区:在运⾏Java程序的时候会产⽣的各种数据会保存在运⾏时数据区
  • 执⾏引擎:执⾏具体的指令(代码) 在这里插入图片描述

1、jvm的三个组成部分

  • 类加载系统
  • 运行时数据区
  • 执行引擎

二、类加载系统

1、类的加载过程

⼀个类被加载进JVM中要经历哪⼏个过程

  • 加载: 通过io流的⽅式把字节码⽂件读⼊到jvm中(⽅法区)
  • 校验:通过校验字节码⽂件的头8位的16进制是否是java魔数cafebabe
  • 准备:为类中的静态部分开辟空间并赋初始化值
  • 解析:将符号引⽤转换成直接引⽤。——静态链接
  • 初始化:为类中的静态部分赋指定值并执⾏静态代码块。

类被加载后,类中的类型信息、⽅法信息、属性信息、运⾏时常量池、类加载器的引⽤等信息会被加载到元空间中。

2、类加载器

  1. 类是谁来负载加载的?——类加载器
  2. Bootstrap ClassLoader 启动类加载器:负载加载jre/lib下的核⼼类库中的类,⽐如rt.jar、charsets.jar
  • ExtClassLoader 扩展类加载器:负载加载jre/lib下的ext⽬录内的类

ext 加载路径:System.getProperty("java.ext.dirs");

  • AppClassLoader 应⽤类加载器:负载加载⽤户⾃⼰写的类

app 加载路径:System.getProperty("java.class.path");

  • ⾃定义类加载器:⾃⼰定义的类加载器,可以打破双亲委派机制。 在这里插入图片描述

三、双亲委派机制

1、双亲委派机制介绍

当类加载进⾏加载类的时候,类的加载需要向上委托给上⼀级的类加载器,上⼀级继续向上委托,直到启动类加载器。启动类加载器去核⼼类库中找,如果没有该类则向下委派,由下⼀级扩展类加载器去扩展类库中,如果也没有继续向下委派,直到找不到为⽌,则报类找不到的异常。

应⽤类加载器怎么加载Student和String呢?需要通过双亲委派机制

在这里插入图片描述

2、为什么要双亲委派机制

防⽌核⼼类库中的类被随意篡改

防⽌类的重复加载

3、双亲委派机制的核心源码

  • ClassLoader.class

4、全盘委托机制

当⼀个类被当前的ClassLoader加载时,该类中的其他类也会被当前该ClassLoader加载。除⾮指明其他由其他类加载器加载。

5、自定义加载器实现双亲委托机制

6、自定义加载器打破双亲委派机制

四、运行时数据区

1、运行时数据区的介绍(也叫JVM的内存模型 JMM、内存区域)

JMM分成了这么⼏个部分

  1. 堆空间(线程共享):存放new出来的对象
  2. 元空间(线程共享):存放类元信息、类的模版、常量池、静态部分
  3. 线程栈(线程独享):⽅法的栈帧
  4. 本地⽅法区(线程独享):本地⽅法产⽣的数据
  5. 程序计数器(线程独享):配合执⾏引擎来执⾏指令 在这里插入图片描述

2、程序在执行时运行数据区的内存变化

线程栈:执⾏⼀个⽅法就会在线程栈中创建⼀个栈帧。

栈帧包含如下四个内容:

局部变量表:存放⽅法中的局部变量

操作数栈:⽤来存放⽅法中要操作的数据

动态链接:存放⽅法名和⽅法内容的映射关系,通过⽅法名找到⽅法内容

⽅法出⼝:记录⽅法执⾏完后调⽤次⽅法的位置。

五、对象的创建流程

1、对象创建流程

在这里插入图片描述

2、类加载校验

校验该类是否已被加载。主要是检查常量池中是否存在该类的类元信息。如果没有,则需要进⾏加载。

3、内存分配

为对象分配内存。具体的分配策略如下:

  • Bump the Pointer(指针碰撞):如果内存空间的分配是绝对规整的,则JVM记录当前剩余内存的指针,在已⽤内存分配
  • Free List(空闲列表):如果内存空间的分配不规整,那么JVM会维护⼀个可⽤内存空间的列表⽤于分配。

对象并发分配存在的问题:

  • Compare And Swap: ⾃旋分配,如果并发分配失败则重试分配之后的地址
  • Thread Local Allocation Buffer(TLAB):本地线程分配缓冲,JVM被每个线程分配⼀空间,每个线程在⾃⼰的空间中创建对象(jdk8默认使⽤,之前版本需要通过-XX:+UseTLAB开启)

4、设置初值

根据数据类型,为对象空间初始化赋值

5、设置对象头

为对象设置对象头信息,对象头信息包含以下内瑞:类元信息、对象哈希码、对象年龄、锁状态标志等

  • 对象头中的Mark Work 字段(32位)

在这里插入图片描述

  • 对象头中的类型指针

类型指针是用来指向元空间当前类的类元信息。⽐如调⽤类中的⽅法,通过类型指针找到元空间中的该类,再找到相应的⽅法。

开启指针压缩后,类型指针只⽤4个字节 储,否则需要8个字节存储

过⼤的对象地址,会占⽤更⼤的带宽和增加GC的压⼒。

对象中指向其他对象所使⽤的指针:8字节被压缩成4字节。 最早的机器是32位,最⼤⽀持内存 2的32次⽅=4G。现在是64位,2的64次⽅可以表示N个T的内存。内存32G即等于2的35次⽅。如果内存是32G的话,⽤35位表示内存地址,这样过于浪费。如果把35位的数据,根据算法,压缩成32位的数据(也就是4个字节)。在保存时⽤4个字节,再使⽤时使⽤8个字节。之前⽤35位保存内存地址,就可以⽤32位保存。这样8个字节的对象,实际上使⽤32位来保存,这样64位就能表示2个对象。如果内存⼤于32G,指针压缩会失效,会强制使⽤64位来表示对象地址。因此jvm堆内存最好不要⼤于32G。

6、执行init方法

为对象中的属性赋值和执⾏构造⽅法。

六、垃圾回收

1、对象成为垃圾的判断依据

在堆空间和元空间中,GC这条守护线程会对这些空间开展垃圾回收⼯作,那么GC如何判断这些空间的对象是否是垃圾,有两种算法:

  • 引⽤计数法:

对象被引⽤,则计数器+1,如果计数器是0,那么对象将被判定为是垃圾,于是被回收。但是这种算法没有办法解决循环依赖的对象。因此JVM⽬前的主流⼚商Hotspot没有使⽤这种算法。

  • 可达性分析算法

    :GC Roots根

    • gc roots根节点: 在对象的引⽤中,会有这么⼏种对象的变量:来⾃于线程栈中的局部变量表中的变量、静态变量、本地⽅法栈中的变量,这些变量都被称为gc roots根节点
  • 判断依据:gc在扫描堆空间中的某个节点时,向上遍历,看看能不能遍历到gc roots根节点,如果不能,那么意味着这个对象是垃圾。

在这里插入图片描述

2、 对象中的finalize方法

Object类中有⼀个finalize⽅法,也就是说任何⼀个对象都有finalize⽅法。这个⽅法是对象被回收之前的最后⼀根救命稻草。

  • GC在垃圾对象回收之前,先标记垃圾对象,被标记的对象的finalize⽅法将被调⽤
  • 调⽤finalize⽅法如果对象被引⽤,那么第⼆次标记该对象,被标记的对象将移除出即将被回收的集合,继续存活
  • 调⽤finalize⽅法如果对象没有被引⽤,那么将会被回收
  • 注意,finalize⽅法只会被调⽤⼀次。

3、对象逃逸

在jdk1.7之前,对象的创建都是在堆空间中创建,但是会有个问题,⽅法中的未被外部访问的对象这种对象没有被外部访问,且在堆空间上频繁创建,当⽅法结束,需要被gc,浪费了性能。所以在1.7之后,就会进⾏⼀次逃逸分析(默认开启),于是这样的对象就直接在栈上创建,随着⽅法的出栈⽽被销毁,不需要进⾏gc。

在栈上分配内存的时候:会把聚合量替换成标量,来减少栈空间的开销,也为了防⽌栈上没

有⾜够连续的空间直接存放对象。

标量:java中的基本数据类型(不可再分)

聚合量:引⽤数据类型。

七、垃圾回收算法

1、标记清除算法、复制算法、标记整理算法、分代回收法

在这里插入图片描述 在这里插入图片描述

2、分代回收算法

在这里插入图片描述

  1. 堆空间被分成了新⽣代(1/3)和⽼年代(2/3),新⽣代中被分成了eden(8/10)、survivor1(1/10)、survivor2(1/10)
  2. 对象的创建在eden,如果放不下则触发minor gc
  3. 对象经过⼀次minorgc 后存活的对象会被放⼊到survivor区,并且年龄+1
  4. survivor区执⾏的复制算法,当对象年龄到达15.进⼊到⽼年代。
  5. 如果⽼年代放满。就会触发Full GC

3、对象进⼊到⽼年代的条件

  • ⼤对象直接进⼊到⽼年代:⼤对象可以通过参数设置⼤⼩,多⼤的对象被认为是⼤对象。

-XX:PretenureSizeThreshold

  • 当对象的年龄到达15岁时将进⼊到⽼年代,这个年龄可以通过这个参数设置:

XX:MaxTenuringThreshold

  • 根据对象动态年龄判断,如果s区中的对象总和超过了s区中的50%,那么下⼀次做复制的时候,把年龄⼤于等于这次最⼤年龄的对象都⼀次性全部放⼊到⽼年代。
  • ⽼年代空间分配担保机制 :在minor gc时,检查⽼年代剩余可⽤空间是否⼤于年轻代⾥现有的所有对象(包含垃圾)。如果⼤于等于,则做minor gc。如果⼩于,看下是否配置了担保参数的配置:-XX: -HandlePromotionFailure ,如果配置了,那么判断⽼年代剩余的空间是否⼩于历史每次minor gc 后进⼊⽼年代的对象的平均⼤⼩。如果是,则直接full gc,减少⼀次minor gc。如果不是,执⾏minor gc。如果没有担保机制,直接full gc。 在这里插入图片描述

八、垃圾回收器

1.Serial收集器

-XX:+UseSerialGC -

XX:+UseSerialOldGC

单线程执⾏垃圾收集,收集过程中会有较⻓的STW(stop the world),在GC时⼯作线程不能⼯作。虽然STW较⻓,但简单、直接。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

2、Parallel收集器

-XX:+UseParallelGC

-XX:+UseParallelOldGC

使⽤多线程进⾏GC,会充分利⽤cpu,但是依然会有stw,这是jdk8默认使⽤的新⽣代和⽼年代的垃圾收集器。充分利⽤CPU资源,吞吐量⾼。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。 在这里插入图片描述

3、ParNew收集器

-XX:+UseParNewGC

⼯作原理和Parallel收集器⼀样,都是使⽤多线程进⾏GC,但是区别在于ParNew收集器可以和CMS收集器配合⼯作。主流的⽅案:

ParNew收集器负责收集新⽣代。CMS负责收集⽼年代。

在这里插入图片描述

4、CMS收集器

-XX:+UseConcMarkSweepGC

⽬标:尽量减少stw的时间,提升⽤户的体验。真正做到gc线程和⽤户线程⼏乎同时⼯作。CMS采⽤标记-清除算法

  • 初始标记: 暂停所有的其他线程(STW),并记录gc roots直接能引⽤的对象。
  • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较⻓但是不需要STW,可以与垃圾收集线程⼀起并发运⾏。这个过程中,⽤户线程和GC线程并发,可能会有导致已经标记过的对象状态发⽣改变。
  • 重新标记:为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。主要⽤到三⾊标记⾥的算法做重新标记。
  • 并发清理:开启⽤户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为⿊⾊不做任何处理。
  • 并发重置:重置本次GC过程中的标记数据。

在这里插入图片描述

5、三⾊标记算法

  • 在并发标记阶段,对象的状态可能发⽣改变,GC在进⾏可达性分析算法分析对象时,⽤三⾊来标识对象的状态
  • 灰⾊:这个对象被GC Roots遍历过但其部分的引⽤没有被GC Roots遍历。在重新标记时重新遍历灰⾊对象。
  • ⽩⾊:这个对象没有被GC Roots遍历过。在重新标记时该对象如果是⽩⾊的话,那么将会被回收。

6、垃圾收集器组合⽅案

不同的垃圾收集器可以组合使⽤,在使⽤时选择适合当前业务场景的组合。

在这里插入图片描述

九、JVM调优实战

在这里插入图片描述

1.JVM调优的核⼼参数

  • -Xss:每个线程的栈⼤⼩。设置越⼩,说明⼀个线程栈⾥能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。
  • -Xms:设置堆的初始可⽤⼤⼩,默认物理内存的1/64
  • -Xmx:设置堆的最⼤可⽤⼤⼩,默认物理内存的1/4
  • -Xmn:新⽣代⼤⼩
  • -XX:NewRatio:默认2表示新⽣代占年⽼代的1/2,占整个堆内存的1/3。
  • -XX:SurvivorRatio:默认8表示⼀个survivor区占⽤1/8的Eden内存,即1/10的新⽣代内存。以下两个参数设置元空间⼤⼩建议值相同,且写死,防⽌在程序启动时因为需要元空间的空间不够⽽频繁full gc。
  • -XX:MaxMetaspaceSize:最⼤元空间⼤⼩
  • XX:MetaspaceSize:元空间⼤⼩,默认是21M,达到该值后会触发Full GC,同时会按100%进⾏动态调整,为了减少⼤数据量占满元空间,频繁触发Full GC,建议在初始化时设置为MaxMetaspaceSize相同的值。

2.JVM调优实战

  • 设置JVM的参数

‐Xms3072M ‐Xmx3072M ‐Xss1M ‐XX:MetaspaceSize=256M

‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 在这里插入图片描述

  • 调整VM参数

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M

‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 在这里插入图片描述

3、调优的关键点

  • 设置元空间⼤⼩,最⼤值和初始化值相同
  • 根据业务场景计算出每秒产⽣多少的对象。这些对象间隔多⻓时间会成为垃圾(⼀般根据接⼝响应时间来判断)
  • 计算出堆中新⽣代中eden、survivor所需要的⼤⼩:根据上⼀条每条产⽣的对象和多少时间成为垃圾来计算出,依据是尽量减少full gc。

4、结合垃圾收集器的调优策略

结合垃圾收集器:PraNew+CMS,对于CMS的垃圾收集器,还需要加上相关的配置:

  • 对于⼀些年龄较⼤的bean,⽐如缓存对象、spring相关的容器对象,配置相关的对象,这些对象需要尽快的进⼊到⽼年代,因此需要配置:-XX:MaxTenuringThreshold=5
  • ⼤对象直接进⼊到⽼年代:-XX:PretenureSizeThreshold=1M
  • CMS垃圾收集器会有并发模式失败的⻛险(转换为使⽤serialOld垃圾收集器),如何避免这种⻛险:将full gc的触发点调低:

-XX:CMSInitiatingOccupancyFraction=85 (默认是92),相当于⽼年代使⽤率达到85%就触发full gc,于是还剩15%的空间允许在cms进⾏gc的过程中产⽣新的对象。

  • CMS垃圾收集器收集完后会产⽣碎⽚,碎⽚需要整理,但不是每次收集完就整理,设置做了3次Full GC之后整理⼀次碎⽚:
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3
  • PraNew+CMS的具体JVM参数配置:

java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -

XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -

XX:+UseParNewGC -XX:+UseConcMarkSweepGC

-XX:CMSInitiatingOccupancyFraction=85 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction=3 -jar device-service.jar

重点作业:

  • 清晰的掌握类加载过程及双亲委派机制
  • 掌握程序在运⾏时 JVM的运⾏时数据区中发⽣了怎样的变化
  • 对象的创建的流程
  • 对象成为垃圾的判断依据
  • 垃圾回收算法有哪些
  • JVM空间内存分配及垃圾回收器的常⽤参数配置

十、JVM性能调优的原则有哪些?

  1. 多数的Java应用不需要在服务器上进行GC优化,虚拟机内部已有很多优化来保证应用的稳定运行,所以不要为了调优而调优,不当的调优可能适得其反
  2. 在应用上线之前,先考虑将机器的JVM参数设置到最优(适合)
  3. 在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间。我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过GC优化令其性能达到一个质的飞跃
  4. GC优化是一个系统而复杂的工作,没有万能的调优策略可以满足所有的性能指标。GC优化必须建立在我们深入理解各种垃圾回收器的基础上,才能有事半功倍的效果
  5. 处理吞吐量和延迟问题时,垃圾处理器能使用的内存越大,即java堆空间越大垃圾收集效果越好,应用运行也越流畅。这称之为GC内存最大化原则
  6. 在这三个属性(吞吐量、延迟、内存)中选择其中两个进行jvm调优,称之为GC调优3选2

十一、什么情况下需要JVM调优?

  • Heap内存(老年代)持续上涨达到设置的最大内存值
  • Full GC 次数频繁
  • GC 停顿(Stop World)时间过长(超过1秒,具体值按应用场景而定)
  • 应用出现OutOfMemory 等内存异常
  • 应用出现OutOfDirectMemoryError等内存异常( failed to allocate 16777216 byte(s) of direct memory (used: 1056964615, max: 1073741824))
  • 应用中有使用本地缓存且占用大量内存空间
  • 系统吞吐量与响应性能不高或下降
  • 应用的CPU占用过高不下或内存占用过高不下

十二、聊聊Java的GC机制

细节可见此博客链接:点我跳转

GC:垃圾回收(Garbage Collection),在计算机领域就是指当一个计算机上的动态存储器(内存空间)不再需要时,就应该予以释放,以让出存储器,便于他用。这种存储器的资源管理,称为垃圾回收。

这三个问题将分别对应接下来的3节一一解答

  • JVM清理的是哪一块的对象?判断垃圾方法
  • 哪些对象会被清理,为什么清理A而不清理B?
  • JVM又是如何清理的?回收算法

十三、CMS 和G1 的区别

1、使用范围不一样

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
  • G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用

2、STW的时间不一样

  • CMS收集器以最小的停顿时间为目标的收集器。
  • G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)

3、垃圾碎片

  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  • G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

4、回收算法不一样

  • CMS :标记-清除”
  • G1:标记-整理

5、大对象处理不一样

  • 在CMS内存中,如果一个对象过大,进入S1、S2区域的时候大于改分配的区域,对象会直接进入老年代。
  • G1处理大对象时会判断对象是否大于一个Region大小的50%,如果大于50%就会横跨多个Region进行存放回收过程不一样

6、回收过程不一样

CMS回收垃圾的4个阶段

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清理
  • 并发重置

G1回收垃圾的4个阶段

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
  1. 初始标记:标记GC Roots 可以直接关联的对象,该阶段需要线程停顿但是耗时短
  2. 并发标记:寻找存活的对象,可以与其他程序并发执行,耗时较长
  3. 最终标记:并发标记期间用户程序会导致标记记录产生变动(好比一个阿姨一边清理垃圾,另一个人一边扔垃圾)虚拟机会将这段时间的变化记录在Remembered Set Logs 中。最终标记阶段会向Remembered Set合并并发标记阶段的变化。这个阶段需要线程停顿,也可以并发执行
  4. 筛选回收:对每个Region的回收成本进行排序,按照用户自定义的回收时间来制定回收计划

初始标记和并发标记和CMS的过程是差不多的,最后的筛选回收会首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

因为采用的标记——整理的算法,所以不会产生内存碎片,最终的回收是STW的,所以也不会有浮动垃圾,Region的区域大小是固定的,所以回收Region的时间也是可控的

同时G1 使用了Remembered Set来避免全堆扫描,G1中每个Region都有一个与之对应的RememberedSet ,在各个 Region 上记录自家的对象被外面对象引用的情况。当进行内存回收时,在GC根节点的枚举范围中加入RememberedSet 即可保证不对全堆扫描也不会有遗漏。

以上就是CMS和G1的对比过程

这是本人今年春招找实习工作准备总结,记录在此,如有需要的老铁可以看看,如有问题可以留言指导

对于 Linux 系统安全来说,日志文件是极其重要的工具。logrotate 程序是一个日志文件管理工具。用于分割日志文件,删除旧的日志文件,并创建新的日志文件,起到“转储&rdqu ...