JVM内存结构

JVM的内存结构主要分为几个区域,首先是堆内存,这个是我们平时开发最常接触的,所有的对象实例和数组都在这里分配,堆内存又分为新生代和老年代,新生代又分为Eden区、Survivor0和Survivor1区。然后是方法区,也叫元空间,存储类的元数据信息,比如类的结构、常量池、静态变量等。还有虚拟机栈,每个线程都有自己的栈,存储局部变量、方法参数、返回值等。程序计数器记录当前线程执行的字节码指令地址。本地方法栈是给native方法用的。

堆内存详解

堆内存是JVM中最大的一块内存区域,主要用来存储对象实例。它分为新生代和老年代两个区域。

新生代

新生代又分为三个区域:

  • Eden区:新创建的对象首先分配在Eden区
  • Survivor0和Survivor1区:也叫S0和S1,用来存放经过一次垃圾回收后存活的对象

新生代的特点是对象生命周期短,大部分对象创建后很快就会被回收。

老年代

老年代用来存放生命周期较长的对象,比如经过多次垃圾回收后仍然存活的对象,或者大对象直接进入老年代。

垃圾回收机制

垃圾回收算法

标记清除算法

这个算法分为两个阶段,首先是标记阶段,遍历所有对象,标记出哪些是垃圾对象,哪些是存活对象。然后是清除阶段,把标记为垃圾的对象回收掉。这个算法的优点是实现简单,缺点就是会产生内存碎片,而且效率不高。

复制算法

复制算法把内存分为两块,每次只用其中一块,垃圾回收的时候,把存活的对象复制到另一块内存中,然后把原来的那块内存全部清空。这个算法的优点是没有内存碎片,缺点就是浪费了一半的内存空间。现在新生代用的就是这种算法的改进版。

标记整理算法

标记整理算法和标记清除算法类似,也是先标记,但是清除的时候不是直接删除,而是把存活的对象向一端移动,然后清理掉边界以外的内存。这个算法的优点是没有内存碎片,缺点就是移动对象需要时间。

分代收集理论

JVM采用分代收集理论,就是根据对象的生命周期不同,采用不同的垃圾回收策略。

新生代垃圾回收(Minor GC)

新生代的对象生命周期短,大部分对象都是朝生夕死,所以采用复制算法。具体过程是这样的:

  1. 新对象首先分配在Eden区
  2. 当Eden区满了,触发Minor GC
  3. 把Eden区和Survivor区中存活的对象复制到另一个Survivor区
  4. 清空Eden区和原来的Survivor区

老年代垃圾回收(Major GC)

老年代的对象生命周期长,采用标记清除标记整理算法。

对象分配策略

对象优先在Eden区分配

新创建的对象首先在Eden区分配,如果Eden区空间不足,触发Minor GC。

大对象直接进入老年代

如果对象很大,比如超过一定阈值,会直接分配到老年代,避免在新生代之间复制。

长期存活的对象进入老年代

对象在Survivor区每经过一次Minor GC,年龄就加1,当年龄达到一定阈值(默认15),就会晋升到老年代。

动态年龄判断

如果Survivor区中相同年龄的对象大小总和超过Survivor区的一半,那么年龄大于等于这个年龄的对象就可以直接进入老年代。

垃圾收集器

Serial收集器

Serial收集器是单线程的垃圾收集器,在进行垃圾回收的时候,必须暂停所有用户线程,也就是Stop The World。这个收集器适合单核CPU或者小内存的应用。

Parallel收集器

Parallel收集器是Serial收集器的多线程版本,使用多个线程进行垃圾回收,提高了回收效率。这是JDK8默认的新生代收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它分为四个阶段:

  1. 初始标记:标记GC Roots能直接关联的对象,需要Stop The World
  2. 并发标记:并发标记所有可达对象
  3. 重新标记:修正并发标记期间变动的对象,需要Stop The World
  4. 并发清除:并发清除垃圾对象

CMS的优点是并发收集,停顿时间短,缺点是会产生内存碎片,而且对CPU资源敏感。

G1收集器

G1(Garbage First)收集器是面向服务端的垃圾收集器,它的特点是把堆内存分成多个大小相等的区域(Region),然后优先回收垃圾最多的区域。G1收集器可以设置期望的停顿时间,通过预测和调整来尽量满足这个时间要求。

ZGC收集器

ZGC是JDK11引入的低延迟垃圾收集器,它的目标是停顿时间不超过10ms,而且停顿时间不会随着堆内存大小增长而增长。

类加载机制

类加载过程

类加载分为五个阶段:加载、验证、准备、解析、初始化。

加载

加载阶段主要是通过类的全限定名获取二进制字节流,然后将字节流转换为方法区的运行时数据结构,最后在内存中生成一个代表这个类的Class对象。

验证

验证阶段主要是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证、符号引用验证。

准备

准备阶段主要是为类变量分配内存并设置初始值,这里的初始值通常是数据类型的零值,比如int是0,boolean是false。

解析

解析阶段主要是将符号引用转换为直接引用,符号引用就是一些字符串,直接引用就是指向内存中具体位置的指针。

初始化

初始化阶段主要是执行类构造器<clinit>()方法,这个方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。

类加载器

启动类加载器(Bootstrap ClassLoader)

启动类加载器负责加载Java的核心类库,比如rt.jar、charsets.jar等,这个类加载器是用C++实现的,是虚拟机的一部分。

扩展类加载器(Extension ClassLoader)

扩展类加载器负责加载Java的扩展类库,比如javax.*开头的类。

应用程序类加载器(Application ClassLoader)

应用程序类加载器负责加载用户类路径(ClassPath)上的类库,这是我们平时开发中接触最多的类加载器。

双亲委派模型

双亲委派模型的工作过程是这样的:

  1. 当一个类加载器收到类加载请求时,首先不会自己去加载,而是委托给父类加载器
  2. 如果父类加载器还有父类加载器,就继续向上委托
  3. 如果父类加载器无法加载,子类加载器才会尝试自己加载

双亲委派模型的优点是保证了类的唯一性,避免了类的重复加载,也保证了Java核心API不被篡改。

JVM调优

堆内存调优

  • -Xms:设置堆内存的初始大小
  • -Xmx:设置堆内存的最大大小
  • -Xmn:设置新生代的大小
  • -XX:SurvivorRatio:设置Eden区和Survivor区的比例

垃圾收集器调优

  • -XX:+UseG1GC:使用G1收集器
  • -XX:MaxGCPauseMillis:设置期望的最大GC停顿时间
  • -XX:+UseConcMarkSweepGC:使用CMS收集器

方法区调优

  • -XX:MetaspaceSize:设置元空间的初始大小
  • -XX:MaxMetaspaceSize:设置元空间的最大大小

常见JVM问题

内存溢出(OutOfMemoryError)

内存溢出通常发生在堆内存、方法区、虚拟机栈等区域。

堆内存溢出

堆内存溢出通常是因为创建了太多对象,或者存在内存泄漏。可以通过增加堆内存大小或者优化代码来解决。

方法区溢出

方法区溢出通常是因为加载了太多类,或者常量池太大。可以通过增加方法区大小或者减少类的加载来解决。

虚拟机栈溢出

虚拟机栈溢出通常是因为递归调用太深,或者栈帧太大。可以通过增加栈大小或者优化递归算法来解决。

内存泄漏

内存泄漏是指程序在运行过程中,不再使用的对象没有被垃圾回收器回收,导致内存占用越来越多。

常见的内存泄漏原因:

  1. 静态集合类:静态集合类持有对象的引用,导致对象无法被回收
  2. 监听器:注册了监听器但没有取消注册
  3. 各种连接:数据库连接、网络连接、IO连接等没有关闭
  4. 内部类和外部类:内部类持有外部类的引用,导致外部类无法被回收

性能监控工具

jps

jps命令可以查看当前运行的Java进程,类似于ps命令。

jstat

jstat命令可以监控JVM的各种统计信息,比如堆内存使用情况、垃圾回收情况等。

jmap

jmap命令可以生成堆内存的dump文件,用于分析内存使用情况。

jstack

jstack命令可以生成线程的dump文件,用于分析线程状态和死锁问题。

VisualVM

VisualVM是一个图形化的JVM监控工具,可以实时监控JVM的各种指标,还可以进行内存分析和线程分析。

面试常见问题

什么是JVM?

JVM是Java虚拟机,它是Java程序运行的环境。JVM的主要作用是:

  1. 加载字节码文件
  2. 解释执行字节码
  3. 管理内存
  4. 进行垃圾回收

为什么Java是跨平台的?

Java是跨平台的,主要是因为JVM的存在。Java程序编译后生成的是字节码文件,这个字节码文件可以在任何安装了JVM的平台上运行。JVM负责将字节码解释成对应平台的机器码执行。

堆内存和栈内存的区别?

堆内存和栈内存的主要区别:

  1. 存储内容:堆内存存储对象实例,栈内存存储局部变量、方法参数等
  2. 生命周期:堆内存中的对象生命周期不确定,栈内存中的变量生命周期确定
  3. 内存管理:堆内存需要垃圾回收,栈内存自动管理
  4. 线程安全:堆内存是线程共享的,栈内存是线程私有的

什么是垃圾回收?

垃圾回收是JVM自动管理内存的机制,它会自动回收不再使用的对象占用的内存。垃圾回收的主要步骤是:

  1. 标记:标记出哪些对象是垃圾
  2. 清除:回收垃圾对象占用的内存
  3. 整理:整理内存,消除碎片

什么时候会触发垃圾回收?

垃圾回收会在以下情况下触发:

  1. 堆内存不足时
  2. 新生代空间不足时
  3. 老年代空间不足时
  4. 手动调用System.gc()时(不推荐)

什么是Stop The World?

Stop The World是指在进行垃圾回收时,JVM会暂停所有用户线程,只保留垃圾回收线程运行。这样做的目的是为了保证垃圾回收的正确性,避免在回收过程中对象状态发生变化。

如何优化JVM性能?

JVM性能优化的主要方法:

  1. 合理设置堆内存大小:根据应用的实际需求设置合适的堆内存大小
  2. 选择合适的垃圾收集器:根据应用的特点选择合适的垃圾收集器
  3. 优化代码:减少对象的创建,避免内存泄漏
  4. 监控和调优:使用监控工具分析性能瓶颈,进行针对性调优

什么是类加载器?

类加载器是JVM用来加载类的组件,它负责将字节码文件加载到内存中,并生成对应的Class对象。Java中有三种主要的类加载器:启动类加载器、扩展类加载器、应用程序类加载器。

什么是双亲委派模型?

双亲委派模型是类加载器的工作机制,当一个类加载器收到类加载请求时,它首先会委托给父类加载器去加载,只有当父类加载器无法加载时,子类加载器才会尝试自己加载。这样可以保证类的唯一性和安全性。