虚拟机种类

  • 一般没有特殊说明,都是针对的是 HotSpot 虚拟机。

    jvm的管理的内存组成

  • 不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个区域:

    • 程序计数器(Program Counter Register)
    • Java 虚拟机栈(Java Virtual Machine Stacks)
    • 本地方法栈(Native Method Stack)
    • Java 堆(Java Heap)
    • 方法区(Methed Area)
  • 线程私有的:
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
  • 线程共享的:

    • 方法区
    • 直接内存 (非运行时数据区的一部分)

      程序计数器

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

  • 在 Java 虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的。在任意一个确定的时刻,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器 。
  • 如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在 Java 虚拟机规范中不会出现 **OutOfMemoryError **的区域。
    • native关键字表明一个方法是c/c++实现的方法,native方法也叫做本地方法
  • 生命周期随着线程的创建而创建,随着线程的结束而死亡

    Java 虚拟机栈

  • 与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。

  • Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
    • 虚拟机栈和栈一样,只支持出栈和入栈2种操作
  • 如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 **StackOverflowError **异常。
  • 如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 **OutOfMemoryError **异常

  • 局部变量表:主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
  • 操作数栈:存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
  • 动态链接主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。

    • 栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError
    • Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

      栈内存stack

      **基本类型变量****对象的引用变量**(如学术类对象s1,对象名s1即学生类对象的引用变量)存放于栈内存内
  • 当创建一个变量时,java会在栈内存中为为该变量自动分配空间,当超过变量的作用域时(即程序执行完了),java会自动清理为该变量分配的内存,该内存空间可以立即另作他用

  • 栈内存的变量实际上是存放在java方法所开辟的栈内存中,java方法将变量逐一放入所开辟的栈内存
  • 栈内存严格来说是指:虚拟机栈中局部变量表,栈内存和堆内存是沿用于c/c++的概念
  • 局部变量表所需的内存空间在编 译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定 的,在方法运行期间不会改变局部变量表的大小

    栈内存与堆内存区别

  • 栈内存的存取速度比堆内存速度快,仅次于位于CPU的寄存器

  • 但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。如基本数据类型能存储的数据大小很明确
  • 另外,栈数据可以共享

int a=3;int b=3;编译器先处理int a=3;在栈内存中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没有找到然后开辟一个地址存放值3;而b创建b引用后找到a的3地址,那它就直接用同一个3地址。
所以a==b 如果Integer a=new Integer(3);``Integer b=new Integer(3);则a!=b
注意Integer a=3;这样会自动拆箱,a还是一个基本类型变量
但是这种字面值的引用并不会影响其他字面值引用。如修改a=4,b还是3

  • 堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

    本地方法栈

  • 本地方法栈与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用Native 方法服务的。

  • 在 Java 虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一了。
  • 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowErrorOutOfMemoryError异常。
    • StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError错误。
      • 栈深超出就是超出栈的大小
    • OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

    Java堆

  • 即我们常说的堆内存,它是jvm中内存最大的一块,被所有线程共享

    • 堆内存用于存放几乎所有new创建的对象实例。和数组(随着jvm的发展的不一定就是所有的对象实例都存堆内存上了,也可能在栈上分配)
      • 从 JDK 1.7 开始已经默认开启逃逸分析,即如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
    • java堆是垃圾收集器管理的内存区域,因此java堆又被称为**GC堆**
  • 堆最容易出现的就是OutOfMemoryError还可以详细分为:
    • OutOfMemoryError: GC Overhead Limit Exceeded当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
    • OutOfMemoryError: Java heap space假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小
  • 堆的内存空间是可以自定义大小的,同时也支持在运行时动态修改,虚拟机实现者可以使用任何垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的;通过 -Xms-Xmx 这两参数去改变堆的初始值和最大值
    • -X 指的是JVM运行参数,ms为最小堆容量/初始堆容量,mx:最大堆容量;如 -Xms256M 代表堆的初始值是 256M ,-Xmx1024M 代表堆的最大值是 1024M 。
    • 一般设置Xms与Xmx相同,避免内存不断增加,也为了避免在 GC(垃圾回收)之后调整堆大小时带来的额外压力。
    • 堆中无内存再进行分配,且堆不可扩展,将抛出OutOfMemoryError
  • 从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
    • jdk7及之前的堆一般分为新生代内存,老生代内存,永久代内存

image.png
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
修正(参见:issue552open in new window :“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。

方法区/元空间

  • 方法区是java8以前的规范,java8取消了方法区,改为了元空间
    • 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
    • 永久代,元空间是方法区的具体实现
    • 方法区是与堆内存连续的物理内存,而元空间则变为了使用本地内存(即整个主机的内存,不再是局限于jvm内存区域,这意味着主要主机还有内存就不会出现oom错误,而方法区只要jvm不再给其分配可用内存而它又满了,就会oom)
    • 方法区与元空间都是用于存储类的结构信息。例如,运行时常量池、字段和方法数据、构造函数和普通方法的字节码等。(常量池是方法区的一部分)
    • 元空间溢出时会得到如下错误:java.lang.OutOfMemoryError: MetaSpace
  • -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。
  • -XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

    常量池

  • 静态常量池:有的地方叫做静态区。静态对象会存储于该区,该区属于方法区/元空间的常量池的一部分

  • 运行时常量池:位于方法区/元空间
  • 字符串常量池:java8后位于堆中。1.8前存储于永久代。可以用XX:StringTableSize指定大小。存储的结构本质就是个HashSet<String>,其中保存的是字符串对象的引用,引用指向堆内存的字符串对象

    • 因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
      1. public class Student {
      2. String name;
      3. int age;
      4. static String school = "郑州大学";
      5. public static void main(String[] args) {
      6. Student s1 = new Student("沉默王二", 18);
      7. Student s2 = new Student("沉默王三", 16);
      8. }
      9. }
      s1,s2这两个引用变量存放在栈区,而name与age存储在堆内存区,而static的school存放在静态区

      JVM简介 - 图3

      java引用

  • Java 的引用的类型有四种:强引用、软引用、弱引用和虚引用。它们背后关系到 JVM 的垃圾回收机制回收内存的时机。

    • 强引用会影响对象被垃圾回收机制回收;而弱引用则对内存的回收无影响。
    • 我们日常写代码通常只会涉及强引用
  • 强引用:我们日常编码涉及的引用全部都是强引用

    • 只要至少还存在一个引用指向一个对象,那么,这个这个对象的内存空间就不会被 JVM 回收。哪怕内存不够用抛出oom也不会进行回收
      Student tom = new Student(); 
      Student jerry = tom;
      Student lucy = tom;
      Student lily = jerry;
      上述代码中,有 4 个引用指向了同一个对象(即无论是直接还是间接引用都指向了同一个堆对象:new Student)。
      

      对象占用内存大小

  • 一个对象在堆内存中占多少内存空间,因 JDK 是 64 位还是 32 位有所区别,但是总体规则是相似的。

  • JVM 在为对象分配内存时,会调整对象中的属性的先后顺序,以压缩内存空间。即,属性在内存中的先后顺序,不一定是你在 Java 类中定义的顺序。
  • 32 位 JDK
    • 8 字节的头部信息;
    • byte、boolean 占 1 字节;
    • char、short 占 2 字节;
    • int、float 占 4 字节;
    • long、double 占 8 字节;
    • 引用占 4 字节;
    • 整体对齐到 4 字节的倍数。
  • 64 位 JDK

    • 12 字节的头部信息;
    • 基本数据类型占字节数与 32 位一样;
    • 引用在占 8 字节(开启『引用压缩』功能后,占 4 字节);
    • 整体对齐到 8 字节的倍数。

      类加载机制

  • class文件中的字节码本质是一个字节数组。它有特定的复杂的内部格式,Java 类初始化的时候会调用 java.lang.ClassLoader 加载字节码,.class 文件中保存着 Java 代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的 .class 文件,并创建对应的 class 对象,将 class 文件加载到虚拟机的内存,而在 JVM 中类的查找与装载就是由 ClassLoader 完成的,而程序在启动的时候,并不会一次性加载程序所要用的所有 class 文件,而是根据程序的需要,来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的

    类加载方式

  • 分为显式与隐式2种

    • 显式:利用反射来加载一个类(即通过反射获取一个Class)
    • 隐式:通过 ClassLoader 来动态加载,new 一个类 或者 类名.方法名 返回一个类 ```java // 1. 反射加载 Class<?> aClass = Class.forName(“java.lang.Runtime”);

// 2. ClassLoader 加载,自动加载的底层就是这样获取Class的 Class<?> aClass1 = ClassLoader.getSystemClassLoader().loadClass(“java.lang.ProcessBuilder”); ```

ClassLoader

  • ClassLoader(类加载器)主要作用就是将 class 文件读入内存,并为之生成对应的 java.lang.Class 对象。
  • JVM 中存在 3 个内置 ClassLoader:
    • BootstrapClassLoader 启动类加载器:负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 **java.xxx.* **都在里面,比如 java.util. 、java.io. 、java.nio. 、java.lang. 等等。
      • **Bootstrap ClassLoader** 并不继承自 ClassLoader ,因为它不是一个普通的 Java 类,底层由 C++ 编写,已嵌入到了 JVM 内核当中。jvm启动时就会启动Bootstrap 类加载器,Bootstrap 加载完核心类库后,再构造Extension ClassLoader 和 App ClassLoader 类加载器(即Bootstrap 加载核心类是在双亲委派之前)
    • ExtensionClassLoader扩展类加载器:负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 **javax** 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中。
    • AppClassLoader系统类加载器:它才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
  • 我们也可以自定义 ClassLoader,自定义的 ClassLoader 都必须直接或间接继承自 java.lang.ClassLoader 类(你可以继承 ExtensionClassLoaderAppClassLoader)。

    类加载流程

  • 类加载大致分为三个步骤:加载、链接、初始化。

    • 加载:读取.class到内存并创建Class对象
    • 链接:把类的二进制数据合并到 JRE 中,其又可分为如下三个阶段:
      • 验证:确保加载的类信息符合 JVM 规范,无安全方面的问题。
      • 准备:为类的静态 Field 分配内存,并设置初始值。
      • 解析:将类的二进制数据中的符号引用替换成直接引用
    • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

      双亲委派机制

  • 双亲委派机制就是确认一个类的加载使用上面三个中一个还是使用我们自定义的ClassLoader

  • 下图简单理解就是向上委派,向下加载。(先走蓝色箭头再走红色箭头)具体就是先走蓝色检查一遍是否有被加载过,都没有再走红色检查哪一个可以进行加载,我们只要记住这三个加载器的层级顺序就能很快把整个图记下来
  • 双亲委派的好处是:
    • 避免重复加载
    • 防止系统级别(即核心类)的类被非法篡改(因为在进行双亲委派时核心类已经被Bootstarp加载过了,因此不会走篡改后的自定义class)
  • JVM简介 - 图4