JVM虚拟机

    JVM是运行Java程序的虚拟机,学习JVM的内存模型,有助于我们了解Java程序运行的基本原理,也能帮助我们调整JVM内存分配以达到更高的程序性能。
    根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。
    211701576919962.jpg

    1. 线程隔离数据区
      1. 程序计数器:内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
      2. 虚拟机栈:线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
      3. 本地方法栈:区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
    2. 线程共享数据区
      1. 堆:对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。
      2. 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    HotSpot虚拟机

    JVM是虚拟机,总的来说是一种标准规范,虚拟机有很多实现版本。而HotSpot是虚拟机的一种实现,它是sun公司开发的,是sun jdk和open jdk中自带的虚拟机,同时也是目前使用范围最广的虚拟机。HotSpot,顾名思义,它是基于热点代码探测的,有JIT即时编译功能,能提供更高质量的本地代码。二者区别是一个是标准,一个是实现方式。

    在日常绝大多数场景里,我们使用的都是HotSpot虚拟机(如下图所示)。HotSpot JVM把堆分为新生代和老年代,分代的目的是为了优化GC性能。其中新生代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。在HotSpot中,设计者将方法区纳入GC分代收集,所以又称永久代。方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。

    对于HotSpot虚拟机,我们主要了解它的垃圾回收机制,即GC。GC分为三种:

    • Minor GC:指对新生代进行垃圾回收;当Eden区满时,触发Minor GC。
    • Major GC:指对老年代进行垃圾回收;许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开。major GC和full GC通常是等价的,收集整个GC堆。
    • Full GC:对整个堆进行垃圾回收。触发条件如下
      • System.gc()方法的调用
      • 老年代空间不足
      • 方法区空间不足
      • 统计数据表明之前Minor GC平均晋升大小比目前老年代剩余的空间大,则不会触发Minor GC而是转为触发Full GC
      • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

    jvm.jpg
    Minor GC

    从新生代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
    一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每经过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到老年代中。

    在GC开始的时候,对象只会存在于Eden区和Survivor区“Fom”,而Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。

    经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会互相交换角色,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到老年代中。

    Full GC

    Full GC是针对整个堆空间进行清理,包括新生代和老年代。如果Full GC后还有无法给新创建的对象分配内存,或者无法移动那些需要进入老年代中的对象,那么JVM抛出OutOfMemoryError。

    关于GC的过程,可以参见这篇文章进行更深入的了解。
    _
    HotSpot JVM参数

    • 堆大小设置
      • -Xmx 设置JVM最大堆内存。
      • -Xms 设置JVM最小堆内存。
      • -Xmn 设置新生代的内存大小。
      • -Xss 设置每个线程的堆栈大小。
      • -XX:NewRatio 设置新生代与年老代的比值。设置为4,则新生代与老年代所占比值为1:4,新生代占整个堆栈的1/5。
      • -XX:SurvivorRatio 设置新生代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。
      • -XX:MaxPermSize 设置永久代内存大小。
      • -XX:MaxTenuringThreshold 设置新生代垃圾最大年龄。如果设置为0的话,则新生代对象不经过Survivor区,直接进入老年代。
      • -XX:PretenureSizeThreshold 当设置了该参数值后,若对象大小大于该数值,则不会分配到Eden区,直接进去老年代
    • 辅助信息
      • -XX:+PrintGC 打印GC日志
      • -XX:+PrintGCDetails 打印GC详细信息
      • -XX:+PrintGCTimeStamps 打印CG发生的时间戳
      • -XX:+PrintGCApplicationConcurrentTime 打印每次垃圾回收前,程序未中断的执行时间

    JDK1.8变化
    **
    JDK1.8移除了永久代,取而代之的是元空间(MetaSpace)。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,理论上取决于32位/64位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。在JDK1.8中,-XX:MaxPermSize参数已经无效,元空间的大小通过-XX:MaxMetaspaceSize来设置。

    以下是JDK在个版本下永久代的一些对比:

    • 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,即HotSpot里的永久代。
    • 在JDK1.7 字符串常量池和静态变量被从方法区拿到了堆中,符号引用(Symbols)转移到了native heap。
    • 在JDK1.8 HotSpot移除了永久代,用元空间(Metaspace)取而代之。