Java 虚拟机在运行时,会把内存空间分为若干个区域。Java 虚拟机所管理的内存区域分为如下部分:方法区、堆内存、虚拟机栈、本地方法栈、程序计数器。
一、JVM 内存结构 - 图1

1、类装载器 ClassLoader

负责加载 class 文件,class 文件在文件开头有特定的文件标识,并且 ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则是由执行引擎(Execution Engine)决定。
虚拟机自带的加载器:
启动类加载器(Bootstrap):由 C++编写,不是前端框架 Bootstrap。
扩展类加载器(Extension):由 Java 语言编写
应用程序类加载器(App):由 Java 语言编写,也叫系统类加载器,加载当前应用的 classpath 的所有类。
用户自定义加载器
Java.lang.ClassLoader 的之类,用户可以定制的加载方式。
类加载器的双亲委派机制
某个特定的类加载器在加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成该加载任务时,才自己去加载。
沙箱机制(防止恶意代码对 java 本身的破坏)
当用户命名了和 Java 一样的类时,Java 会首先加载自带的类。

2、方法区

方法区是线程共享的,通常用来保存装载的类的元结构信息。主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。字符串池
在 jdk1.7 及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间)。也有人用“永久代”表示方法区。
在 jdk1.8 中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。
一、JVM 内存结构 - 图2

3:堆内存

一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类,方法,穿变量放到堆内存中,New 创建对象在堆内存
它是 JVM 管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。在垃圾收集的层面上来看,由于现在收集器基本上都采用分代收集算法,因此堆还可以分为新生代(YoungGeneration)和老年代(OldGeneration),新生代还可以分为 Eden、FromSurvivor、To Survivor。
新生代和老年代的默认比例为 1:2,也就是说新生代占用 1/3的堆内存,而老年代占用 2/3 的堆内存。
可以通过参数 -XX:NewRatio=2 来设置老年代/新生代的比例。
JAVA1.7 如下图,但在 Java1.8 中,其他基本没变,只是将 Perm 变成了元空间
一、JVM 内存结构 - 图3

3.1:新生代

在方法中去 new 一个对象,那这方法调用完毕后,对象就会被回收,这就是一个典型的新生代对象。
新生代中的对象 98%都是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块比较大的 Eden 空间和两块较小的 Survivor 空间
刚刚新建的对象在 Eden 中,经历一次 Minor GC,Eden 中的存活对象就会被移动到第一块 survivor space S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0 中的存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为这种复制算法保证了 S1 中来自 S0 和 Eden 两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0 和 Eden 被清空,然后下一轮 S0 与 S1 交换角色,如此循环往复。如果对象的复制次数达到 16 次,该对象就会被送到老年代中。
每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
当 Survivor 空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。同时,长期存活的对象将进入老年代

3.2:老年代

对象何时进入老年代?

  • 大对象直接进入老年代
  • 长期存活的对象:每熬过一回 Minor GC,对象 age +1 ,当年龄达到一定数值时(JDK7 是 15,可以通过参数-XX:MaxTenuringThreshold 设置年龄阀值)
  • 当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄。

    3.3:永久代

    2:诊断
    Jmap:查看堆内存占用情况 jmap - heap 进程 id

    4:程序计数器

    作用:记住下一条 jvm 指令的执行地址
    特点:是线程私有的,不会存在内存溢出
    程序计数器是一块非常小的内存空间,可以看做是当前线程执行字节码的行号指示器,每个线程都有一个独立的程序计数器,因此程序计数器是线程私有的一块空间,此外,程序计数器是 Java 虚拟机规定的唯一不会发生内存溢出的区域。

    5:虚拟机栈

    1:虚拟机栈也是每个线程私有的一块内存空间,它描述的是方法的内存模型。
    虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。
    :2:栈内存溢出:栈帧过多导致栈内存溢出 栈帧过大导致栈内存溢出

    6、本地方法栈

    虚拟机栈执行的是 Java 方法,本地方法栈执行的是本地方法(NativeMethod),其他基本上一致

    7:元空间

    上面说到,jdk1.8 中,已经不存在永久代(方法区),替代它的一块空间叫做“元空间”,和永久代类似,都是 JVM 规范对方法区的实现,但是元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制,但可以通过-XX:MetaspaceSize 和-XX:MaxMetaspaceSize 来指定元空间的大小。如果不指定元空间的大小,默认情况下,元空间最大的大小是系统内存的大小,元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。
    如果元空间内存不够用,就会报OOM,默认情况下,对应一个64位的服务端JVM来说,其默认的-XX:MetaspaceSize值为21MB,这就是初始的高水位线,一旦元空间的大小触及这个高水位线,就会触发Full GC并会卸载没有用的类,然后高水位线的值将会被重置。建议将-XX:MetaspaceSize设置为较高的值,而-XX:MaxMetaspaceSize不进行设置。即-1 无限制。
    比如 8/16G 的,设置 256M,最大设置 512M

    JVM 关闭

    正常关闭:

  • 所有守护线程执行结束

  • System.exit()
  • ctrl + C
  • kill (-15) SIGTERM 信号

异常关闭

  • RuntimeException
  • OOM

强制关闭

  • kill -9 SIGKILL 信号
  • Runtime.halt()
  • 断电
  • 系统关机
  • 系统 carsh

转载 https://www.yuque.com/jykss/jykss/qaomdf