【面试题】4.JVM相关

本文详细讲解了Java的JVM相关面试题,涵盖了JVM基础、GC算法与收集器、类加载与双亲委派模型以及JVM调优实战。主要内容包括堆和栈的区别、Java内存结构、堆内存划分、内存模型、对象访问定位、OOM原因分析、GC算法(如标记-清除、复制、标记-整理)以及Full GC与Minor GC的触发条件。此外,还探讨了类文件结构、字节码执行引擎和JVM调优经验,涉及了如何判断和解决GC引发的问题,如高内存占用、服务可用性下降、迁移应用后的频繁FGC、服务器周期性宕机和高CPU占用等情况。
摘要由CSDN通过智能技术生成

4.1 JVM基础

4.1.1 堆和栈的区别?

  • 栈内存是存储方法帧和局部变量(基本类型的变量、对象的引用变量),方法调用完后会释放该栈及栈中变量。存取速度比堆要快,仅次于寄存器,多个引用可以指向同一个地址,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
  • 堆内存用于存放由new创建的对象和数组,由JVM管理,由于要在运行时动态分配内存,存取速度较慢,
  • 栈中的变量指向堆内存中的变量,这就是 Java 中的指针

4.1.2 Java的内存结构(运行时数据区域)?

  • 程序计数器:记录当前线程所执行到的字节码的行号,若线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址,若是native方法,则计数器值为空。是JVM中唯一一个不会发生OOM的区域。
  • 虚拟机栈: 每个方法被执行的时候都会创建一个栈帧,一个栈帧包含:局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 本地方法栈:为JVM所调用到的Nativa(本地方法)服务的,
  • 堆:存储对象实例,更好地分配内存。堆是GC管理的主要区域。为了更好地回收内存。在物理上可以不连续,只要逻辑上连续即可。
  • 方法区:堆的一个逻辑部分,但却是非堆,保存运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等信息。引用地址,JDK7之前会将对象拷贝到常量池。
  • 运行时常量池:存放编译期生成的各种字面量和符号引用,运行期间也可以将新的常量放入池中,例如String类的intern()方法,JDK7中若常量池中无数据,则会从堆中引用。
  • 堆和方法区是线程共享的,而栈是线程独享的。OOM:给一个对象一直分配内存,超过最大大小限制或本地直接内存已满,会抛出OOM,SOF:递归调用方法,
  • 内存泄露:一个不再使用的对象或变量却被引用而一直被占据在内存中,如当 o对象的引用被置空后,若发生 GC,o对象不能被 GC 回收,因为 GC 在跟踪代码栈中的引用时,会发现 v 引用,尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。从而导致内存泄露,还有数据库的连接、网络连接、IO连接等没有显示调用关闭,也会造成内存泄露。例如:static List list = new ArrayList<>();Object o = new Object();list.add(o);o=null;

4.1.3 堆内存的划分?

  • JDK8之前堆的内存区域分为新生代、老年代 和永久区,
    • 新生代:所有新生成的对象首先都是放在新生代的。以尽可能快速的收集掉那些生命周期短的对象,它被细分为 Eden 、 Survivor from 和Survivor to。默认Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),而JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,总有一块 Survivor 区域空闲。因此,新生代实际可用的内存空间为90% 的新生代空间。
    • 老年代:在新生代中经历了N次(默认15次)垃圾回收后仍然存活的对象,就会被放到老年代中。因此,老年代中存放的都是一些生命周期较长的对象。默认老年代与新生代的比例的值为 2:1 ( 通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。
    • 永久区:用于存放Class和Meta的信息,Class在被 Load的时候被放入永久区,它和存放Instance的Heap区域不同,
  • JDK8中移除永久区,引入Metaspace(元空间),元数据(类信息、常量、静态变量)都在本地内存中分配,元数据由Klass (class文件在jvm里的运行时数据结构,默认放在 Compressed Class Pointer Space 中,是一块连续的内存区域,紧接着 Heap)和NoKlass Metaspace(常量池)组成,由多块不连续的内存组成。类加载器用SpaceManager管理元空间。用元数据替代永久区的原因是永久代中容易出现性能问题和内存溢出,并且指定大小困难。
  • Metaspace内存的分配:JVM 通过 mmap 接口向 OS 申请内存映射,每次申请 2MB 空间(虚拟内存映射),只有在使用时才会真正消耗主存的 2MB。申请的内存放到一个链表中 VirtualSpaceList,作为其中的一个 Node。
  • 如果设置了-XX:-UseCompressedClassPointers,或者 -Xmx 设置大于 32 G,就不会有Compressed Class Pointer Space这块内存,这时Klass 都会存在 NoKlass Metaspace 里。
  • Metaspace中的类被卸载的三个必要条件:①该类的实例都被回收;②加载该类的classloader被回收;③该类对应的java.lang.class对象没有任何地方被引用。
  • IO 交互型(RPC、MQ)大部分对象在 TP9999 的时间内都会死亡, Young 区越大越好。MEM 计算型(如Hadoop、HBase)对内存要求高,对象存活时间长,Old 区越大越好。

4.1.4 Java的内存模型?

  • Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成
  • 内存间交互操作
    • lock:作用于主内存的变量,把一个变量标识为一条线程独占状态。
    • unlock:作用主存,把一个处于锁定状态的变量释放出来
    • read:作用主存,把变量值从主内存传输到工作内存中,以便之后load
    • load:作用工作内存,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
    • use:作用工作内存,把工作内存中的一个变量值传递给执行引擎,
    • assign:作用工作内存,它把一个从执行引擎接收到的值赋值给工作内存的变量
    • store:作用工作内存,把工作内存中变量的值传送到主内存中,以便之后write
    • write:作用主存,它把store操作从工作内存中一个变量的值传送到主内存的变量中
    • 读写操作必须按顺序执行、lock和unlock必须成对出现、不允许read和load、store和write操作之一单独出现、对一个变量执行unlock操作之前,必须先把此变量同步到主内存中
  • 执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序
    • as-if-serial:不管怎么重排序,程序的执行结果不能被改变
    • 为了保证内存的可见性,编译器在生成指令序列的适当位置会插入内存屏障指令来禁止处理器重排序
    • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存

4.1.5 对象的访问定位?

  • Java 对象内存布局:对象头区域(包括MarkWord 和Class Pointer)、实例数据区域 、对齐填充区域。数组对象和非数组对象的区别就是需要额外的空间存储数组的长度length。
    java对象内存布局
    • 对象头区域:①对象自身的运行时数据( MarkWord 64位OS8字节):存储 hashCode、GC 分代年龄、锁类型标记、偏向锁线程 ID 、 CAS 锁指向线程 LockRecord 的指针等, synconized 锁的机制与这个部分( markword )密切相关,用 markword 中最低的三位代表锁的状态,其中一位是偏向锁位,另外两位是普通锁位。②对象类型指针( Class Pointer ):对象指向它的类元数据的指针、 JVM 就是通过它来确定是哪个 Class 的实例。-XX:+UseCompressedOops参数可以压缩指针。
    • 实例数据区域 :存储的是对象真正有效的信息,比如对象中所有字段的内容
    • 对齐填充区域:JVM 的实现 HostSpot 规定对象的起始地址必须是 8 字节的整数倍,HotSpot 为了高效读取对象,就做了"对齐"。
  • Java程序通过虚拟机栈上的reference数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定。分为句柄和直接指针(HotSpot虚拟机)两种。
    • 句柄:堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针。
    • 直接指针:reference中存储的就是对象地址,相当于一级指针。
  • 两种访问方式的优点:句柄 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。而直接指针速度快,它节省了一次指针定位的时间开销。
  • Java 中对象地址操作主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法,分配方法有两种:分配对象的方法:①空闲链表(free list): 通过额外的存储记录空闲的地址,将随机 IO 变为顺序 IO,但带来了额外的空间消耗。②碰撞指针(bump pointer): 通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。

4.1.6 发生OOM的原因?

  • 1.堆溢出:java.lang.OutOfMemoryError: Java heap space
    • 出现原因:存在大对象分配或内存泄漏。
    • 解决方法:检查是否有大对象的分配,jmap把堆内存dump下来使用MAT分
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值