06.JVM学习

学习JVM

1. JVM概念

介于操作系统和Java应用程序之间。成为java的"翻译官,把字节码翻译成各种操作系统能识别的机器码,实现"一次编写,到处运行"。

【.java 源码】--(javac编译)-->【.class字节码】--(JVM解释)-->【机器码】

2. JVM的体系结构

整体流程:加载-->存储-->执行。

【加载.class文件到内存】--->【类加载子系统】--->【运行时数据区】--->【执行引擎】--->【本地方法入口】

  • 类加载子系统,负责加载.class文件
  • 运行时数据区,属于JVM的内存,PC寄存器,栈,堆,都属于这里。
  • 执行引擎, 包括解释器,JIT编译器,GC,是真正的执行代码核心。

3. 类加载器

作用:把磁盘上的.class文件加载到JVM的方法区,并在堆中创建对应的Class对象

类加载器 实现语言 加载范围
启动类加载器(Bootstrap) C/C++ JDK 核心类(rt.jar 中的 java.lang/String 等)
扩展类加载器(Extension) Java JRE/lib/ext 目录下的扩展类
应用程序类加载器(App) Java 自定义类(classpath 下的代码)
自定义类加载器 Java 自定义类(classpath 下的代码)

4. 双亲委派机制

类加载器的核心规则,先委托父类加载器加载,加载不到的类才到自己加载

  • 流程(以加载自定义类com.test.MyClass为例)
    App类加载器-->委托Extension-->委托Bootstrap-->Bootstrap找不到(只加载核心类)-->Extension不到-->App自己加载。
  • 核心作用
  1. 安全,防止自定义java.lang.String替换JDK核心类。
  2. 唯一, 同一个类只会被加载一次,避免重复加载。

5. 沙箱安全机制

JVM的安全围栏,限制java程序的运行范围,防止恶意代码破坏系统核心。引用了一个Domain(域)的概念,JVM会将不同的代码加载到不同的域当中。

  • 字节码校验器:检查字节码是否合规(比如禁止修改内存地址)。
  • 类加载器+双亲委派:只加载可信类。
  • 安全管理器(SecurityManager):控制程序权限(如禁止读写文件,执行系统命令等)
  • 存取控制器(access controller) :控制核心API对操作系统的存取权限。
  • 安全软件包(security package): java.security之下的包和类,允许用户为自己的应用增加安全特性。包括:
    安全提供者; 消息摘要; 数字签名; 加密; 鉴别。

6. Native方法实现本地库底层调用

声明:native方法-->javah生成C头文件-->C/C++实现-->编译成动态库-->Java通过System.loadLibrary()加载。

注意:凡是代有native关键字的方法,说明java的作用范围达不到了,会去调用底层C语言库,即进入本地方法栈。JNI融合不同语言。

eg. private native void start0(); //本地方法常用于硬件驱动,网络系统中很少使用了。

7. PC寄存器(程序技术器)

  • 作用:存储当前线程正在执行的JVM指令地址(如果是执行native方法,值为undefined);
  • 特点:不会OOM(内存溢出),因为大小固定。
  • 类比:像书签,随时可以在线程切换时从书签位置继续执行。

8. 方法区

所有线程共享的内存区,存储类的"元数据"(类名,方法,字段,常量池),静态变量,JIT编译后代码

版本变化:

  • JDK1.7及之前:叫永久代,属于堆的一部分,大小有限。
  • JDK1.8及之后:移除永久代,改用元空间(Metaspece),直接使用操作系统内存,只受OS内存限制,大幅减少内存溢出概率。
  • JVM内部过程:
    (方法区包含:static 数据和类,final,Class,常量池)
    运行时数据区(Runtime Data Area)---> 方法区Method Area<--->执行引擎-垃圾重灾区--解释器interpreter --JIT编辑器--垃圾回收器
    --->java栈 Stack
    --->本地方法栈Native Method Stack<--->本地方法接口JNI(Java Native Interface)<---本地方法库。(不存在垃圾回收)
    --->堆 Heap(垃圾重灾区)
    --->程序计数器 Program Counter 不存在垃圾回收)

9. 栈(虚拟机栈)

线程私有:每个线程创建时分配一个栈,由多个栈帧组成。

  • 栈帧: 每个方法调用时创建一个栈帧,存储局部变量,方法返回地址,方法执行完栈帧出栈。
  1. 栈的大小可以通过 -Xss设置,如-Xss1m.
  2. 递归过深,会触发栈溢出(StackOverflowError)。
  3. 无需GC, 栈帧的创建销毁都是自动的。
  • 栈就像"弹夹",方法调用时进行"压弹",方法结束完,子弹弹出。容量有限,压太多会溢出。

10. 三种主流JVM

JVM实现 开发商 特点 应用场景
HotSpot Oracle 混合解释+JIT编译,性能优主流 绝大多数Java应用
JRockit BEA 无解释器,全JIT编译,适合高并发 已被Oracle整合
IBM J9 IBM 轻量,跨平台,内存占用少 IBM WebSphere等

命令行执行:java -version,就会显示对应JVM,例如:Java HotSpot(TM) 64-Bit Server VM.

11. 堆

是JVM中最大的内存区域,所有线程共享,用于存储所有new出来的对象,也是GC(垃圾回收)核心区。一个JVM只有一个堆内存。

  • 核心参数:
    • -Xms: 堆的初始大小,建议和-Xmx一致,避免JVM动态调整;
    • -Xmx: 堆最大大小,如-Xms4g -Xmx4g,堆固定4G;
  • 特点:会触发OOM,因为对象太多,堆内存不足。(OutOfMemery)
  • 比喻:就是JVM的大仓库,所有线程元素都往里放,满了就报错,GC是仓库清理工,清理没用的对象数据。

12. 新生区(新生代)

堆的一部分,专门存储刚创建的对象,分为 3 个区域(默认比例 8:1:1):

  • Eden 区(伊甸园):新对象优先分配到这里;
  • Survivor 0(S0)、Survivor 1(S1):Eden 满了触发 Minor GC(轻量 GC),存活对象移到 - - S0/S1,来回移动达到年龄阈值(默认 15)后,移到老年代。

13. 永久区(永久代)

⚠️ 仅 JDK 1.7 及之前存在,是方法区的实现,存储类元数据、静态变量、常量池。

  • 问题:大小有限(-XX:PermSize/-XX:MaxPermSize),加载大量类时易 OOM;
  • JDK 1.8 替代方案:元空间(Metaspace),使用操作系统内存,默认无上限,大幅减少 OOM。

14. 堆内存调优

核心目标: 减少GC频率/耗时, 提升程序性能,核心步骤+参数;

  1. 监控 GC:用jstat -gc 进程ID、jvisualvm、Arthas 查看 GC 次数 / 耗时;
  2. 调整核心参数(JDK 1.8):
  • -Xms4g -Xmx4g:堆初始 / 最大大小设为 4G(固定大小避免动态调整);
  • -Xmn2g:新生代设为 2G(占堆的 1/2,适合大多数场景);
  • -XX:SurvivorRatio=8:Eden:S0:S1=8:1:1;
  • -XX:+UseG1GC:使用 G1 收集器(适合大堆、低延迟);
  1. 分析 OOM:添加-XX:+HeapDumpOnOutOfMemoryError,导出堆快照用 MAT 工具分析内存泄漏。

15. GC(垃圾回收)

JVM 自动回收堆中 "无用对象" 的机制,无需程序员手动管理内存(对比 C++ 的new/delete)。

  • 垃圾判定:主流用可达性分析(以 GC Roots 为起点,无引用链的对象是垃圾);

  • GC 类型:

    • Minor GC:新生代 GC,频率高、耗时短;
    • Major GC:老年代 GC,频率低、耗时长(通常伴随 Minor GC);
    • Full GC:整堆 GC(新生代 + 老年代 + 元空间),耗时最长,应尽量避免;
  • 核心收集器:Serial(串行)、Parallel(并行,JDK8 默认)、G1(JDK9 默认)。

16. JMM(Java内存模型)

⚠️ 不是 JVM 的内存区域(堆 / 栈),而是一套规范,解决多线程的可见性、原子性、有序性问题。

  • 核心抽象:

    • 主内存:所有线程共享,存储共享变量(对应堆);
    • 工作内存:线程私有,存储共享变量副本(对应栈);
  • 核心保障:

    • 可见性:volatile关键字保证一个线程修改的变量,其他线程立刻可见;

    • 原子性:synchronized/Atomic 类保证操作要么全执行,要么全不执行;

    • 有序性:volatile/synchronized禁止指令重排,保证代码执行顺序和编写顺序一致。