JVM学习与实践

一、JVM组成

  1. JVM是什么
    • Java Virtual Machine,Java程序的运行环境,负责将Java代码转换为字节码并执行。
    • 好处包括一次编写,到处运行,以及自动内存管理和垃圾回收机制。
  2. JVM的组成部分及运行流程
    • 组成部分:ClassLoader(类加载器)、Runtime Data Area(运行时数据区)、Execution Engine(执行引擎)、Native Method Library(本地库接口)。
    • 运行流程:
      • 类加载器将Java代码转换为字节码。
      • 运行时数据区将字节码加载到内存,执行引擎将字节码翻译为底层系统指令,交由CPU执行,同时调用本地库接口实现程序功能。
  3. 程序计数器
    • 线程私有的,内部保存字节码的行号,用于记录正在执行的字节码指令的地址。
    • Java虚拟机通过线程轮流切换和分配执行时间来实现多线程,程序计数器用于在线程切换时恢复到上一次执行的行号。
    • 是JVM规范中唯一一个没有规定出现OOM的区域,不会进行GC。
  4. Java堆
    • 线程共享的区域,主要用于保存对象实例、数组等,当堆中没有内存空间可分配给实例或无法再扩展时,会抛出OutOfMemoryError异常。
    • Java 8中,年轻代被划分为Eden区和两个大小严格相同的Survivor区,老年代主要保存生命周期长的对象,元空间保存类信息、静态变量、常量、编译后的代码等。
    • 为避免方法区出现OOM,Java 8将堆上的方法区(永久代)移动到本地内存,开辟了元空间,其本质和永久代类似,都是对JVM规范中方法区的实现,但元空间使用本地内存,默认情况下大小仅受本地内存限制。
  5. 虚拟机栈
    • 每个线程运行时所需的内存,由多个栈帧组成,每个栈帧对应一次方法调用时所占用的内存。
    • 每个线程只能有一个活动栈帧,对应当前正在执行的方法。
    • 垃圾回收主要针对堆内存,栈帧弹栈后内存会释放。栈内存分配不是越大越好,默认通常为1024k,栈帧过大会导致线程数变少。
    • 方法内的局部变量如果没有逃离方法的作用范围是线程安全的,否则需要考虑线程安全。
    • 栈内存溢出情况包括栈帧过多导致的栈内存溢出(典型问题如递归调用)和栈帧过大导致的栈内存溢出。
  6. 方法区
    • 各个线程共享的内存区域,主要存储类的信息、运行时常量池。
    • 虚拟机启动时创建,关闭虚拟机时释放。
    • 常量池可以看作是一张表,虚拟机指令根据其中的信息找到要执行的类名、方法名、参数类型、字面量等。
    • 运行时常量池是常量池在类加载时的体现,将常量池中的符号地址变为真实地址。
  7. 直接内存
    • 不受JVM内存回收管理,是虚拟机的系统内存,常见于NIO操作时,用于数据缓冲区,分配回收成本较高,但读写性能高。
  8. 堆栈的区别
    • 栈内存一般用于存储局部变量和方法调用,堆内存用于存储Java对象和数组;堆会进行GC垃圾回收,而栈不会。
    • 栈内存是线程私有的,堆内存是线程共有的。
    • 两者异常错误不同,栈空间不足会抛出java.lang.StackOverFlowError,堆空间不足会抛出java.lang.OutOfMemoryError。

二、类加载器

  1. 类加载器的定义和种类
    • 类加载器用于装载字节码文件(.class文件),主要职责是将指定的类找到或生成对应的字节码文件,并负责加载程序所需的资源。
    • 类加载器根据各自加载范围的不同,划分为启动类加载器(BootStrap ClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)和自定义类加载器。
  2. 双亲委派模型
    • 类加载器在接到加载类的请求时,首先会把请求委托给父类加载器去完成,只有父类加载器无法完成任务时,才由下一级加载器加载。
  3. JVM采用双亲委派机制的原因
    • 避免某一个类被重复加载,保证唯一性。
    • 保证类库API不会被修改,防止恶意篡改核心API库。
  4. 类装载的执行过程
    • 类的生命周期包括加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备和解析统称为连接。
    • 加载阶段通过类的全名获取类的二进制数据流,并将其解析为方法区内的数据结构,创建java.lang.Class类的实例作为访问入口。
    • 验证阶段进行文件格式验证、元数据验证、字节码验证和符号引用验证,确保类符合JVM规范和安全性。
    • 准备阶段为类变量分配内存并设置初始值,static变量的分配和赋值在不同阶段完成。
    • 解析阶段将类中的符号引用转换为直接引用。
    • 初始化阶段对类的静态变量、静态代码块执行初始化操作,若父类尚未初始化则优先初始化父类,且多个静态变量和静态代码块按自上而下的顺序执行。
    • 使用阶段JVM从入口方法开始执行用户的程序代码,调用静态类成员信息、创建对象实例等。
    • 卸载阶段当用户程序代码执行完毕后,JVM销毁创建的Class对象,最后退出内存。

三、垃圾回收

  1. Java垃圾回收机制简介
    • 为了让程序员专注于代码实现,不用过多考虑内存释放问题,Java语言有自动的垃圾回收机制(GC),程序员只需关心内存申请,内存释放由系统自动完成。
    • 不同对象引用类型在垃圾回收时采用不同的回收时机,自动垃圾回收算法很重要,不合理的算法可能导致内存溢出。
  2. 对象可被垃圾回收的条件
    • 如果一个或多个对象没有任何引用指向它,那么这个对象就是垃圾,可能会被垃圾回收器回收。
    • 确定垃圾的方式有引用计数法和可达性分析算法,现在的虚拟机主要采用可达性分析算法。
    • 引用计数法中,对象被引用一次,引用次数递增,当引用次数为0时可回收,但存在循环引用的问题。
    • 可达性分析算法通过根节点(GC Roots)向下遍历,判断对象是否与根节点有直接或间接的引用,若无则可回收。当对象被标记为可回收后,在GC时会判断其是否执行过finalize方法,若未执行则会执行,在方法中可设置对象与GC Roots关联,若再次判断仍不可达则会回收,否则不回收,且finalize方法对每个对象只会执行一次。
  3. JVM垃圾回收算法
    • 标记清除算法:将垃圾回收分为标记和清除两个阶段,解决了引用计数法中的循环引用问题,但效率较低,且会导致内存碎片化。
    • 复制算法:将内存空间一分为二,每次只用其中一块,垃圾回收时将存活对象复制到另一块,然后清空原空间,适合垃圾对象多、内存使用率低的情况。
    • 标记整理算法:在标记清除算法的基础上进行优化,将存活对象向内存一端移动,然后清理边界以外的垃圾,解决了碎片化问题,但效率有一定影响。
    • 分代收集算法:在Java 8中,堆被分为新生代和老年代,新生代内部又分为Eden区、S0区和S1区。当对新生代产生GC(Minor GC)时,若Eden区内存不足,会标记存活对象并复制到S0区(或S1区),然后清空Eden区;当S0区(或S1区)内存不足时,会标记存活对象并复制到S1区(或S0区)。当幸存区对象熬过几次回收(最多15次)或幸存区内存不足、大对象导致提前晋升时,会晋升到老年代。Minor GC发生在新生代,暂停时间短;Mixed GC是新生代 + 老年代部分区域的垃圾回收,G1收集器特有;FullGC是新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免。
  4. JVM垃圾回收器
    • 串行垃圾收集器(Serial和Serial Old):使用单线程进行垃圾回收,堆内存较小,适合个人电脑,Serial作用于新生代,采用复制算法,Serial Old作用于老年代,采用标记 - 整理算法,垃圾回收时所有线程都要暂停。
    • 并行垃圾收集器(Parallel New和Parallel Old):多个线程进行垃圾回收,JDK 8默认使用,Parallel New作用于新生代,采用复制算法,Parallel Old作用于老年代,采用标记 - 整理算法,垃圾回收时所有线程都要暂停。
    • CMS(并发)垃圾收集器:针对老年代的垃圾回收器,以获取最短回收停顿时间为目标,应用仍能正常运行,采用标记 - 清除算法。
    • G1垃圾收集器:应用于新生代和老年代,划分成多个区域,每个区域都可以充当Eden、Survivor、Old、Humongous,采用复制算法,响应时间与吞吐量兼顾,分成年轻代回收、并发标记、混合收集三个阶段,若并发失败会触发Full GC。
  5. 强引用、软引用、弱引用、虚引用的区别
    • 强引用:只有所有GC Roots对象都不通过强引用引用该对象时,该对象才能被垃圾回收。
    • 软引用:仅有软引用引用该对象时,在垃圾回收后内存仍不足时会再次触发垃圾回收。
    • 弱引用:仅有弱引用引用该对象时,在垃圾回收时无论内存是否充足都会回收弱引用对象。
    • 虚引用:必须配合引用队列使用,被引用对象回收时会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。

四、JVM实践(调优)

  1. JVM调优参数的设置位置
    • tomcat的设置:修改TOMCAT_HOME/bin/catalina.sh文件,设置JAVA_OPTS参数。
    • springboot项目jar文件启动:在linux系统下直接加参数启动springboot项目,如nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &。
  2. JVM调优的参数
    • 设置堆的初始大小(-Xms)和最大大小(-Xmx),通常设置为相同值以防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外时间。
    • 设置年轻代中Eden区和两个Survivor区的大小比例(-XXSurvivorRatio),默认比例为8:1:1,可根据程序情况调优。
    • 设置年轻代的初始大小(-XX:newSize)和最大大小(-XX:MaxNewSize),初始大小和最大大小通常相同,年轻代和老年代默认比例为1:2。
    • 线程堆栈的设置(-Xss),每个线程默认堆栈为1M,一般256K就够用,减少堆栈可产生更多线程,但受操作系统限制。
    • 使用-Xmn设置年轻代的大小,当survivor区不够大或占用量达到50%时,会把一些对象放到老年代,可通过设置合理的eden区、survivor区及使用率避免full GC。
    • 设置参数XX:PetenureSizeThreshold,当对象大小超过该阈值(单位为B)时,在老年代(tenured)分配内存空间。
    • 通过-XX:MaxTenuringThreshold设置对象晋升到老年代的年龄阈值,若想让对象留在年轻代,可设置较大阈值。
    • -XX:+UseParallelGC用于年轻代使用并行垃圾回收收集器,关注吞吐量,减少垃圾回收时间。
    • -XX:+UseParallelOldGC设置老年代使用并行垃圾回收收集器。
    • -XX:+LargePageSizeInBytes使用大的内存分页增加CPU的内存寻址能力,提升系统性能。
    • -XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿。
  3. JVM调优的工具
    • 命令工具:
      • jps(Java Process Status):输出JVM中运行的进程状态信息。
      • jstack:查看java进程内线程的堆栈信息。
      • jmap:用于生成堆转存快照,可显示Java堆的信息,通过jmap -dump:format=b,file=heap.hprof pid生成二进制格式的堆转存快照。
      • jhat:用于分析jmap生成的堆转存快照,但一般不推荐使用,而是使用Ecplise Memory Analyzer。
      • jstat:JVM统计监测工具,可显示垃圾回收信息、类加载信息、新生代统计信息等,如jstat -gcutil pid总结垃圾回收统计,jstat -gc pid显示垃圾回收统计信息。
    • 可视化工具:
      • jconsole:基于jmx的GUI性能监控工具,可对jvm的内存、线程、类进行监控,打开方式为在java安装目录bin目录下直接启动jconsole.exe。
      • VisualVM:故障处理工具,能够监控线程、内存情况,查看方法的CPU时间和内存中的对象、已被GC的对象、反向查看分配的堆栈,打开方式为在java安装目录bin目录下直接启动jvisualvm.exe。
  4. java内存泄露的排查思路
    • 通过jmap指定打印内存快照dump,可通过配置参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/app/dumps/在出现OutOfMemoryError异常时自动生成dump文件。
    • 使用VisualVM(Ecplise MAT)等工具分析dump文件,VisualVM可加载离线的dump文件,通过文件-->装入选择dump文件查看堆快照信息。
    • 通过查看堆信息定位内存溢出的代码行号。
    • 找到对应的代码,阅读上下文进行修复。
  5. CPU飙高排查方案与思路
    • 使用top命令查看占用cpu的情况,找到占用cpu较高的进程。
    • 通过ps H -eo pid,tid,%cpu | grep 进程id查看当前线程中的进程信息,找到占用cpu较高的线程id。
    • 将线程id转换为16进制,通过jstack 进程id找到有问题的线程,进一步定位到问题代码的源码行号。

五、面试现场

  1. JVM组成
    • 候选人介绍了JVM的四个组成部分(ClassLoader、Runtime Data Area、Execution Engine、Native Method Library)及其运行流程,详细说明了运行时数据区的各个部分(堆、方法区、栈、本地方法栈、程序计数器)的作用,重点阐述了程序计数器记录线程执行字节码的行号、Java堆的存储内容和内存分区(年轻代、老年代)、方法区存储的信息以及虚拟机栈的内存模型和与堆的区别。
  2. 类加载器
    • 候选人解释了类加载器的作用是将字节码文件加载到JVM中,列举了常见的类加载器(启动类加载器、扩展类加载器、应用类加载器、自定义类加载器),描述了类装载的执行过程(加载、验证、准备、解析、初始化、使用、卸载)和双亲委派模型(类加载器收到加载请求时先委派给父类加载器,只有父类无法完成时才自己尝试加载),并说明了JVM采用双亲委派机制的原因(避免类重复加载、保证类库API安全)。
  3. 垃圾回收
    • 候选人简述了Java垃圾回收机制的目的(让程序员专注代码实现,自动管理内存释放)和GC的作用,解释了对象可被垃圾回收的条件(无引用指向时为垃圾,通过可达性分析算法确定,引用计数法存在循环引用问题),介绍了JVM垃圾回收算法(标记清除算法、复制算法、标记整理算法、分代收集算法)和垃圾回收器(串行垃圾收集器、并行垃圾收集器、CMS垃圾收集器、G1垃圾收集器),以及强引用、软引用、弱引用、虚引用的区别。
  4. JVM实践(调优)
    • 候选人说明了JVM调优参数的设置位置(tomcat中修改catalina.sh文件,springboot项目在启动命令中添加参数)和常用的调优参数(堆大小、年轻代和老年代的相关参数、线程堆栈、垃圾回收器等),介绍了调试JVM使用的工具(jps、jstack、jmap、jstat、jconsole、VisualVM),以及java内存泄露的排查思路(生成dump文件、分析dump文件、定位问题代码、修复代码)和CPU飙高的排查方案(使用top命令、ps命令、jstack命令定位问题线程和代码行号)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值