具体学习 JVM 可以参考我以前写的这篇博客 对Java虚拟机的学习总结,本篇博客在该博客的基础上加以修改
1 Java 内存区域
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为以下6个运行时数据区域。
- 程序计数器:一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空。
- Java 虚拟机栈:与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
- Java 堆:对大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区:与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是 JVM 规范中定义的一个概念,具体放在哪里,不同的实现可以放在不同的地方。
- 运行时常量池:运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
2 finalize() 方法工作原理
一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其 finalize() 方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
至于为什么在下一次垃圾回收动作发生时才会回收内存,原因是如果一个对象覆盖了 finalize() 方法,那么在真正被宣告死亡的时候,至少需要经过两次标记。第一次被标记的时候会被放在 一个 F-Queue 队列中。finalize() 方法是对象逃脱死亡命运的最后一次机会,在第二次标记的时候,如果该对象成功与引用链(GC-Roots)上的任何一个对象关联,那么它仍然可以存活下来,否则将会被垃圾收集器回收。
3 Java 8 的内存分代改进
在 jdk1.8 中对内存模型中方法区的实现永久代进行了移除,取而代之的是元空间。原因是在方法区中实现垃圾回收的条件比较苛刻,因此存在着内存溢出的风险。在 jdk1.8 之后,当方法区内存使用较多时,元空间会使用物理内存,减少了风险。
4 OOM
4.1 什么是 OOM?
OOM,即 Out Of Memory,官方说法是当 JVM 因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个 error。总而言之,就是指没有空闲内存,并且垃圾收集器也无法提供更多内存。
4.2 怎么排查 OOM?
可以查看服务器运行日志以及项目记录的日志,捕捉到内存溢出异常。
4.3 OOM 出现在什么时候?哪些会导致 OOM?
- JAVA 堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起,可以通过虚拟机参数 -Xms,-Xmx 等修改。
- JAVA 永久代溢出,即方法区溢出了,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见 ,尤其是在运行时存在大量动态类型生成的场合( jdk8 已经没有方法区了,改为元数据区)
- JAVA 虚拟机栈溢出,不会抛 OOM error,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数 -Xss 来设置栈的大小。程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
- 直接内存不足,也会导致 OOM。
5 GC
5.1 什么是 GC?
GC,即就是 Java 垃圾回收机制。GC 触发的条件有两种,一是程序调用 System.gc 时可以触发,一是系统自身来决定 GC 触发的时机。
5.2 什么是 Minor GC?
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。当 Eden 区满时,触发 Minor GC。
5.3 什么是 Full GC?
Full GC 是清理整个堆空间,包括年轻代和老年代。
5.4 Full GC 的触发条件
- 调用 System.gc 时,系统建议执行 Full GC,但是不一定执行
- 老年代空间不足
- 方法区空间不足
- 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
- 由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
5.5 垃圾回收的两次标记
- 第一次标记:对于一个没有其他引用的对象,筛选该对象是否有必要执行 finalize() 方法,如果没有执行必要,则意味可直接回收。(筛选依据:是否覆写或执行过 finalize 方法;因为 finalize 方法只能被执行一次)。
- 第二次标记:如果被筛选判定位有必要执行,则会放入 FQueue 队列,并自动创建一个低优先级的 finalize 线程来执行释放操作。如果在一个对象释放前被其他对象引用,则该对象会被移除 FQueue 队列。