JVM相关

一、运行时数据区域

  1. 程序计数器:通过改变值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都要依赖此计数器。此内存区域是唯一一个没有OutOfMemoryError的区域。
  2. Java虚拟机栈:非线程共享,每个线程维护一个栈,存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(非对象本身)、和returnAddress类型(指向一条字节码指令地址)。会抛出StackOverflowError和OutOfMemoryError。
  3. 本地方法栈:为Native方法服务。
  4. Java堆:被所有线程共共享,存储所有的对象实例以及数组,被所有线程共享;java堆是垃圾收集器管理的主要区域(GC堆);会抛出OutOfMemoryError。
  5. 方法区:用于存储已被虚拟机加载的类信息、常量(String常量池也在这里)、静态变量(static变量、static方法)、即时编译器编译后的代码等数据;属于堆的一个逻辑部分,被各个线程共享,被GC管理,会抛出OutOfMemoryError。
  6. 运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量(常量)和符号引用。

二、垃圾回收

1、可达性分析算法:通过一系列成为GC Roots的对象作为起始点向下搜索,走过的路径成为引用链,当一个对象到GC Roots没有任何引用链时,证明此对象不可用。

4208827c9ded5e1eb98c98b707e43be5f1c.jpg

可作为GC Roots的对象包括:

  • 虚拟机栈中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中引用的对象。

2、垃圾收集算法

新生代:复制法

将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

老年代:标记-整理法

553fd90fb9cf0b65b0039c12fd0589dccab.jpg

标记-清除

0c877f584afe31a68b4c419c328f545deb3.jpg

这种会造成大量内存碎片,所以不建议使用。

另:

  • 对象优先在新生代分配;
  • 大对象直接进入老年代;
  • 新生代发生垃圾回收时,如果另一个survival不够大,则存活对象进入老年代;
  • 长期存活的对象进入老年代。

三、JVM启动参数

Xms  Xmx 堆的最小、最大容量

Xmn 新生代大小

Xss 每个线程的栈大小

四、JVM调优

常用的JVM调优工具:

  • jps:虚拟机进程状况工具,可以输出JVM启动时的指定参数;
  • jstat:虚拟机统计信息监视工具,可以查看新生代、老年代内存使用情况,垃圾回收次数;
  • jmap:Java内存印象工具,可以生成堆转储快照;
  • jhat:虚拟机堆转储快照分析工具,分析jmap生成的快照;(分析同样一个dump快照,MAT需要的额外内存比jhat要小的多的多,所以建议使用MAT来进行分析)
  • jstack:Java堆栈跟踪工具,可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源
  • jinfo:Java配置信息工具,这个命令作用是实时查看和调整虚拟机运行参数, 之前的jps -v口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用jinfo口令。

参考:https://www.cnblogs.com/warehouse/p/9479104.html        https://www.jianshu.com/p/aaee11115f37

一次调优:

1、匿名内部类引起的内存泄漏:

匿名内部类或者普通的内部类,会持有外部类的引用。如果内部类的生命周期比外部类长,就会导致外部类也无法被回收。解决办法:将内部类改成静态内部类,如果需要外部类的引用,则手动持有一个外部类的弱引用。

public void doSmthing(T t){
	redis.addListener(new Listener(){
		public void onTimeout(){
			if(t.success()){
				//执行操作
			}
		}
	});
}

由于listener在回调后不会进行释放,而且回调是个超时的操作,当某个事件超过了设定的时间(1分钟)后才会进行回调,这样就导致了T这个对象始终无法回收,所以内存中会存在这么多对象实例。

public class SampleActivity extends Activity {
 
  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  };
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);
 
    // Go back to the previous Activity.
    finish();
  }
}

当这个Activityfinish掉的时候,Handler发送的这个Message会继续在主线程的message queue中存在1000*60*10ms才会被处理掉。这个Message持有了activityHandler对象的引用,并且Handler内部类又隐式地持有它的外部类SampleActivity的引用,在Message被处理之前,这个引用链会一直存在,从而会阻止activitycontext被垃圾回收器回收,这样就会导致这个activity引用的所有resources造成内存泄漏。(Message被处理掉之后,再遇到GC,该Message对象就会被回收,其引用的Handler对象也会被回收,相应的activity也就可以被回收了,如果没有其他引用继续引用它时)。代码中的new Runnable也是同理。非静态的匿名内部类也会隐式地持有外部类的引用,也会造成内存泄漏。

public class SampleActivity extends Activity {

  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  /**
   * Instances of anonymous classes do not hold an implicit
   * reference to their outer class when they are "static".
   */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
    
    // Go back to the previous Activity.
    finish();
  }
}

2、查全表

3、新生代太小导致youngGC频繁

-Xmn350M -> -Xmn800M
-XX:SurvivorRatio=4 -> -XX:SurvivorRatio=8 //Eden区和两个Survivor比例从4:3:3改为8:1:1
-Xms1000m ->-Xms1800m

五、new一个对象的过程

虚拟机遇到一条new指令时,检查是否能在运行时常量池中定位到类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程,然后初始化对象、创建对象。

一、加载和初始化

1、加载

使用双亲委派模型加载对象,由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例;

2、验证

格式验证:验证是否符合class文件规范

语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)

操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

3、准备

为类中的所有静态变量分配内存空间,并为其设置一个初始值;

4、解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。·    

5、初始化(先父后子)

给静态变量赋值、执行static代码块

二、创建对象

1、在堆区分配对象需要的内存(指针碰撞法或空闲列表法)

  • 指针碰撞,当虚拟机使用复制算法或标记整理算法实现的垃圾收集器时,内存区域都是规整的,这时候使用指针碰撞分配内存,用过的内存放在一边,空闲的内存在另一边,中间用一个指针作为分界点,当需要为新对象分配内存时只需把指针向空闲的一边移动一段与对象大小相等的距离。
  • 空闲列表,当虚拟机使用标记清除算法实现的垃圾收集器时,内存都是碎片化的,那虚拟机就要记录哪块内存是可用的,当需要分配内存时,找一块足够大的内存空间给对象实例,并更新记录。

2、对所有实例变量赋默认值

3、执行实例初始化代码

初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法

4、如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它

六、GC停顿以及SafePoint

GC停顿(Stop The World)的原因是,在进行垃圾回收时,需要做可达性分析。在枚举那些可以作为GC Root的类时,不可以出现对象引用关系还在发生变化的情况,这就是导致GC进行时必须停止所有java执行线程的一个重要原因,即使是号称几乎不会发生停顿的CMS收集器,枚举根节点时也是必须要停顿的。发生停顿的这个点叫做SafePoint。

可作为GC Root的对象主要在全局引用和执行上下文中,逐个检查会消耗很多时间。HotSpot中一个叫做OopMap的数据结构,直接存放了对象引用的地址。在OopMap的帮助下可以快速准确的完成GC Root的枚举。但是导致对象引用关系变化(或者说导致OopMap内容变化)的指令有很多,不可能为每一条指令都生成OopMap,只是在特定位置记录了这些内容,这些特定的位置就是SafePoint。

SafePoint的选取标准是那些会导致程序长时间执行的指令,比如方法调用、循环跳转、异常跳转等,这些指令才会产生SafePoint。

需要执行GC时,线程不会立刻中断,而是执行到下一个安全点才中断。

七、垃圾收集器

1、serial

单线程,且在进行垃圾收集的时候必须停止其他线程;用在新生代时使用复制算法,用在老年代时使用 “标记-整理“ 算法;简单高效,单CPU环境下没有线程开销,仍可用在Client模式下的新生代。

2、parnew

多线程版本的serial,但在发生垃圾回收时仍然要停止其他线程;

3、serial old 

老年代版本的serial

4、parnew old

老年代版本的parnew

5、CMS(concurrent mark sweep)

JDK1.5发布,旨在缩短垃圾收集的停顿时间,垃圾回收和用户线程可以并发执行,采用 “标记-清除” 算法,常用在老年代。其垃圾回收经历四个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

初始标记只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段会深入查找GC Roots关联到的对象,比较耗时;重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记,这个过程的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记时间短。

优点:

  • 并发收集
  • 低停顿

缺点:

  • 比较耗费CPU资源
  • 无法处理浮动垃圾(由于CMS并发清理阶段用户线程还在运行,会有新的垃圾不断产生)
  • 由于是基于 “标记-清除” 算法,在垃圾收集结束之后会产生大量空间碎片。

6、G1

JDK1.7提出。

垃圾回收的过程:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

优点:

  • 并发回收,和用户线程同时执行,低停顿;
  • 不用和其他收集器配合,独自管理新生代和老年代;
  • 采用“ 标记-整理 ”算法,不会产生碎片;
  • 可预测的停顿。

关于可预测的停顿,在G1之前的收集器的收集范围是整个新生代或者老年代,而G1则是将整个Java堆划分成多个大小相等的Region(新生代和老年代的概念还在,但是不再是物理隔离,它们都是一部分Region的集合)。之所以能建立可预测的时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾回收,先回收价值最大的Region。

转载于:https://my.oschina.net/u/2286010/blog/3093649

<think>好的,用户现在问的是JVM相关的高频面试题。之前他们已经询问过n8n和Dify的面试题,现在转向JVM,说明可能在准备Java方向的面试,特别是后端开发或者系统架构相关职位。需要确保回答覆盖JVM的核心知识点,并且提供清晰、有深度的解析。 首先,我需要回忆常见的JVM面试题,比如内存结构、垃圾回收机制、类加载过程、性能调优等。用户可能希望得到详细的解答,而不仅仅是问题列表。所以每个问题需要给出关键点和示例代码或配置。 然后,要注意用户之前的问题结构,他们喜欢示例代码和对比表格,比如在之前的回答中使用了代码块和表格。因此,在回答JVM问题时,也应该采用类似的格式,比如用代码块展示JVM参数配置,用表格对比不同GC算法的优缺点。 接下来,要确保问题覆盖高频考点。例如,内存模型、GC算法、类加载机制、常见问题排查、性能调优参数等。同时,每个问题需要简明扼要的解释,避免冗,但又要足够详细,让读者能理解核心概念。 另外,用户可能希望了解如何将理论应用到实际工作中,比如如何分析内存泄漏、如何配置JVM参数来优化应用性能。因此,在回答中加入实际案例或配置示例会更有帮助。 最后,需要检查回答是否符合用户指定的格式,包括在末尾提出3-5个相关问题,并确保相关问题内容紧密相关,能够引导用户进一步深入学习。</think>以下是 JVM 高频面试题及详细解析,涵盖内存模型、GC 机制、性能调优等核心知识点: --- ### 1. **JVM 内存模型(运行时数据区)** ```java // 示例:JVM 内存区域划分 public class JVMStructure { // 程序计数器(线程私有) private int counter; // Java 堆(所有线程共享) private Object obj = new Object(); // Java 栈(线程私有) public void method() { int localVar = 10; // 局部变量存在栈帧中 } // 方法区(所有线程共享,存储类元数据) static final String CONSTANT = "Hello"; } ``` **关键点**: - **线程私有区域**:程序计数器、虚拟机栈、本地方法栈 - **线程共享区域**:Java 堆、方法区(JDK8 后 Metaspace 替代永久代) --- ### 2. **垃圾回收机制(GC)** **GC 算法对比**: | 算法 | 优点 | 缺点 | 适用场景 | |-------------|--------------------------|--------------------------|----------------------| | 标记-清除 | 简单高效 | 产生内存碎片 | 短暂对象多的场景 | | 标记-整理 | 消除碎片 | 效率较低 | 老年代 | | 复制 | 无碎片,效率高 | 空间利用率低 | 年轻代(Survivor 区)| | 分代收集 | 综合前三种优点 | 实现复杂 | 现代 JVM 默认策略 | **常见 GC 器**: - **Serial**:单线程 GC(适用于单核 CPU) - **Parallel Scavenge**:多线程吞吐优先(`-XX:+UseParallelGC`) - **CMS**:低延迟(标记-清除,易碎片) - **G1**:分区回收(平衡吞吐延迟,`-XX:+UseG1GC`) --- ### 3. **类加载机制** **双亲委派模型流程**: ```java // 类加载器层次结构 public class ClassLoaderDemo { public static void main(String[] args) { System.out.println(ClassLoader.getSystemClassLoader()); // 应用类加载器 System.out.println(ClassLoader.getSystemClassLoader().getParent()); // 扩展类加载器 System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); // 引导类加载器(null) } } ``` **关键点**: - **加载顺序**:Bootstrap → Ext → System → 自定义 - **作用**:保证类的唯一性和安全性(如 `java.lang.Object` 不被篡改) --- ### 4. **JVM 性能调优参数** ```bash # 常见 JVM 参数配置 java -Xms512m -Xmx512m \ # 初始/最大堆内存 -Xmn256m \ # 年轻代大小 -XX:SurvivorRatio=8 \ # Eden/Survivor 比例 -XX:+UseG1GC \ # 使用 G1 GC -XX:MaxGCPauseMillis=200 \ # 目标停顿时间 -XX:G1HeapRegionSize=4M \ # 区域大小 -XX:+PrintGCDetails \ # 输出 GC 详情 -jar myapp.jar ``` --- ### 5. **内存泄漏排查步骤** **实战流程**: 1. **监控指标**:通过 `jstat -gc <pid>` 观察老年代使用率 2. **生成堆快照**:`jmap -dump:format=b,file=heap.bin <pid>` 3. **分析工具**:使用 MAT(Memory Analyzer Tool)查看支配树 4. **典型场景**: - **静态集合类未清理**(如 `static Map`) - **监听器未注销**(如 `ServletContextListener`) - **线程池未关闭** --- ### 6. **JVM 内存溢出类型及解决** | 异常类型 | 原因及解决方案 | |------------------------|------------------------------------------| | `OutOfMemoryError: Java heap space` | 调整堆大小(`-Xmx`),检查内存泄漏 | | `OutOfMemoryError: Metaspace` | 增加元空间(`-XX:MaxMetaspaceSize`) | | `StackOverflowError` | 递归过深或线程栈大小不足(`-Xss`) | | `OutOfMemoryError: Direct buffer memory` | 减少 `ByteBuffer.allocateDirect()` 使用 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值