WinddSnow

Java面试题04JVM

字数统计: 2k阅读时长: 7 min
2022/10/22

JDK1.8 JVM 运行时内存

  1. 程序计数器:
    线程私有的(每个线程都有一个自己的程序计数器), 是一个指针. 代码运行, 执行命令. 而每个命令都是有行号的,会使用程序计数器来记录命令执行到多少行了.记录代码执行的位置
  2. Java 虚拟机栈:
    线程私有的(每个线程都有一个自己的 Java 虚拟机栈). 一个方法运行, 就会给这个方法创建一个栈帧, 栈帧入栈执行代码, 执行完毕之后出栈(弹栈)存引用变量,基本数据类型
  3. 本地方法栈:
    线程私有的(每个线程都有一个自己的本地方法栈), 和 Java 虚拟机栈类似, Java 虚拟机栈加载的是普通方法,本地方法加载的是 native 修饰的方法.
    native:在 java 中有用 native 修饰的,表示这个方法不是 java 原生的.
  4. :
    线程共享的(所有的线程共享一份). 存放对象的,new 的对象都存储在这个区域.还有就是常量池.
  5. 元空间: 存储.class 信息, 类的信息,方法的定义,静态变量等.而常量池放到堆里存储

JDK1.8 和 JDK1.7 的 jvm 内存最大的区别是, 在 1.8 中方法区是由元空间(元数据区)来实现的, 常量池.
1.8 不存在方法区,将方法区的实现给去掉了.而是在本地内存中,新加入元数据区(元空间).

JDK1.8 堆内存结构

Young 年轻区(代): Eden+S0+S1, S0 和 S1 大小相等, 新创建的对象都在年轻代

Tenured 年老区: 经过年轻代多次垃圾回收存活下来的对象存在年老代中.

Jdk1.7 和 Jdk1.8 的区别在于, 1.8 将永久代中的对象放到了元数据区, 不存永久代这一区域了.

Gc 垃圾回收

JVM 的垃圾回收动作可以大致分为两大步,首先是「如何发现垃圾」,然后是「如何回收垃圾」。说明一点, 线程私有的不存在垃圾回收, 只有线程共享的才会存在垃圾回收, 所以堆中存在垃圾回收.

如何发现垃圾

Java 语言规范并没有明确的说明 JVM 使用哪种垃圾回收算法,但是常见的用于「发现垃圾」的算法有两种,引用计数算法和根搜索算法

引用计数算法

该算法很古老(了解即可)。核心思想是,堆中的对象每被引用一次,则计数器加 1,每减少一个引用就减 1,当对象的引用计数器为 0 时可以被当作垃圾收集。

优点:快。
缺点:无法检测出循环引用。如两个对象互相引用时,他们的引用计数永远不可能为 0。

根搜索算法(也叫可达性分析)

根搜索算法是把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即可以当作垃圾。

Java 中可作为 GC Root 的对象有

  1. 虚拟机栈中引用的对象
  2. 本地方法栈引用的对象
  3. 方法区中静态属性引用的对象
  4. 方法区中常量引用的对象

如何回收垃圾

ava 中用于「回收垃圾」的常见算法有 4 种:

标记-清除算法(mark and sweep)

分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。

缺点:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片。

标记-整理算法

是在标记-清除算法基础上做了改进,标记阶段是相同的,但标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。

优点:内存被整理后不会产生大量不连续内存碎片。

复制算法(copying)

将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。

缺点:可使用的内存只有原来一半。

分代收集算法(generation)

当前主流 JVM 都采用分代收集(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为年轻代、年老代、永久代,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

年轻代(Young Generation)

  1. 所有新生成的对象首先都是放在年轻代的。
  2. 新生代内存按照8:1:1 的比例分为一个eden区和两个Survivor(survivor0,survivor1)区。大部分对象在 Eden 区中生成。回收时先将 eden 区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也存放满了时,则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区,然后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,然后将 survivor0 区和 survivor1 区交换,即保持 survivor1 区为空, 如此往复。
  3. 当 survivor1 区不足以存放 eden 和 survivor0 的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次 Full GC,也就是新生代、老年代都进行回收。
  4. 新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高(不一定等 Eden 区满了才触发)

年老代(Old Generation)

  1. 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  2. 内存比新生代也大很多(大概是 2 倍),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率比较高。

持久代(Permanent Generation)

用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,从 JDK8以后已经废弃, 将存放静态文件,如 Java 类、方法等这些存储到了元数据区.

JVM 调优参数

这里只给出一些常见的性能调优的参数及其代表的含义。(大家记住 5.6 个就行, 并不需要都记住.)

  • -Xmx3550m:设置 JVM 最大可用内存为 3550M。
  • -Xms3550m:设置 JVM 初始内存为 3550m。注意:此值一般设置成和-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。
  • -Xmn2g:设置年轻代大小为 2G。整个 JVM 内存大小=年轻代大小 + 年老代大小 + 持久代大小。此值对系统性能影响较大,Sun 官方推荐配置为整个堆的 3/8。
  • -Xss256k:设置每个线程的栈大小。JDK5.0 以后每个线程栈大小为 1M,以前每个线程栈大小为 256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
  • -XX:NewRatio=4:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代)。设置为 4,则年轻代与年老代所占比值为 1:4。(该值默认为 2)
  • -XX:SurvivorRatio=4:设置年轻代中 Eden 区与 Survivor 区的大小比值。设置为 4,则两个 Survivor 区与一个 Eden 区的比值为 2:4。
CATALOG
  1. 1. JDK1.8 JVM 运行时内存
  2. 2. JDK1.8 堆内存结构
    1. 2.0.1. Young 年轻区(代): Eden+S0+S1, S0 和 S1 大小相等, 新创建的对象都在年轻代
    2. 2.0.2. Tenured 年老区: 经过年轻代多次垃圾回收存活下来的对象存在年老代中.
  • 3. Gc 垃圾回收
    1. 3.1. 如何发现垃圾
      1. 3.1.0.1. 引用计数算法
      2. 3.1.0.2. 根搜索算法(也叫可达性分析)
  • 3.2. 如何回收垃圾
    1. 3.2.0.1. 标记-清除算法(mark and sweep)
    2. 3.2.0.2. 标记-整理算法
    3. 3.2.0.3. 复制算法(copying)
    4. 3.2.0.4. 分代收集算法(generation)
  • 3.3. 年轻代(Young Generation)
  • 3.4. 年老代(Old Generation)
  • 3.5. 持久代(Permanent Generation)
  • 4. JVM 调优参数