JVM深入理解

Java虚拟机(JVM)

1、JVM的位置

 

2、JVM体系结构

 

3、类加载器

 

 

3.1、类加载器的作用

  • 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口

 

3.2、类加载器的分类

  • 引导类加载器:C++编写的,JVM自带的类加载器,负责Java平台核心库,用来装载核心类库,该加载器无法直接获取

  • 扩展类加载器:主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库

  • 系统类加载器:最常用的加载器,主要负责加载classpath所指定的位置的类或者是jar文档

  • 用户自定义类加载器:用户自定义的类加载器,可加载指定路径的class文件

 

3.3、类加载步骤分析

//以这段代码为例
public class Test03 {
   public static void main(String[] args) {
       AAA aaa = new AAA();
  }
}

class AAA{
   static {
       System.out.println("类AAA的静态代码代码块");
  }

   static int num = 100;

   public AAA() {
       System.out.println("类AAA的构造方法");
  }
}
  • 加载(Loading)

    • 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象

  • 链接(Linking):将Java类的二进制代码合并到JVM的运行状态之中的过程

    • 验证:确保加载的信息符合JVM规范,没有安全方面的问题

    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配

    • 解析:虚拟机常量池内的符号引用(常量名)替换成直接引用(地址)的过程

  • 初始化

    • 执行类构造器方法<clinit>()的过程,类构造器方法<clinit>()是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的

    • 当初始化一个类时,如果发现其父类还没有初始化,则需要先触发其父类的初始化

    • 虚拟机会保证一个类的<clinit>()方法在多线程中会被正确加载和同步

 

3.4、双亲委派机制

  • 概念

    当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

  • 图解

  • 作用

    • 从一定程度上可以防止危险代码的植入,保证代码安全,如果有人想替换系统级别的类:String.java。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载。

    • 防止重复加载同一个类,通过委托机制,如果级别高的加载器加载过了,就不用再加载一遍。

 

4、本地方法栈

 

5、程序计数器

  • 每个线程都有自己私有的的一个程序计数器,一个指针,指向方法区中的方法字节码(用来存储指向下一条指令,也就是即将要执行的指令的地址),在执行引擎读取下一条指令。

  • 程序计数器所占内存空间非常小,几乎可以忽略不计。

 

6、栈

 

栈结构解析

  • 局部变量表

 

  • 操作数栈

  • 动态连接

    在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。

    每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

  • 返回地址

    • 当一个方法开始执行后,有两种方式可以结束方法:

      • 正常完成出口:

        当程序遇到返回指令,会将返回值传递给上层的方法调用者,一般来说,调用者的PC计数器可以当成返回地址。

      • 异常完成出口:

        当程序执行遇到异常,并且没有处理或者抛出异常,就会导致方法退出,此时方法没有返回值,返回地址需要通过异常处理表来确定。

    • 当方法返回时,可能执行的操作:

      • 恢复上层方法的局部变量表和操作数栈

      • 把返回值压入调用者栈帧的操作数栈

      • 调整栈帧PC计数器的值以执行方法调用后面的一条指令

 

7、方法区

  • jdk8后在堆中的元空间中

  • 被所有线程共享的一片区域

  • 存放:静态的(static)、常量的(final)、类信息(构造方法、接口定义)、运行时的常量池

 

8、堆

  • 一个JVM只有一个堆内存,大小可以调节

  • 类的具体实例、类中常量、变量、方法,保存所有引用类型的真实对象

 

8.1、堆结构

  • 堆结构详解

    • 新生区

      类诞生、生长、以及可能死亡的地方

      • 伊甸园:所有对象的实例化都是在伊甸园产生的

      • 幸存区:伊甸园中经过垃圾回收后还存活的对象会进入到幸存区

    • 老年区

      新生区中经过垃圾回收后还存活的对象会进入到老年区

    • 永久区

      存储的是Java运行时的一些环境或类信息,JVM被关闭后会释放这个区域的内存,不存在垃圾回收

      永久区是逻辑上存在但是物理上不存在的一片区域

      • jdk 1.6 之前:名为永久代,此时常量池在方法区中

      • jdk 1.7:名为永久代,但永久代慢慢退化,此时常量池在堆中

      • jdk 1.8 之后:名为元空间,此时常量池在元空间中

      //查看堆内存具体各区域所占空间大小,本例中虚拟机的总内存为2M

      //查看虚拟机的总内存,代码如下
      long totalMemory = Runtime.getRuntime().totalMemory();
      //输出结果:虚拟机的总内存为:2.0MB
      System.out.println("虚拟机的总内存为:" + (totalMemory/(double)1024/1024) + "MB");

      //打印GC详细信息
      Heap
      PSYoungGen    total 1536K,
          used 1455K [0x00000000ffd80000, 0x00000000fff80000, 0x0000000100000000)
          eden space 1024K,
              94% used [0x00000000ffd80000,0x00000000ffe71e10,0x00000000ffe80000)
          from space 512K,
              95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
          to   space 512K
              0% used [0x00000000ffe80000,0x00000000ffe80000,0x00000000fff00000)
      ParOldGen    total 512K,
          used 128K [0x00000000ff800000, 0x00000000ff880000, 0x00000000ffd80000)
          object space 512K,
              25% used [0x00000000ff800000,0x00000000ff820000,0x00000000ff880000)
      Metaspace    used 3505K, capacity 4500K, committed 4864K, reserved 1056768K
       class space    used 389K, capacity 392K, committed 512K, reserved 1048576K
                   
      //结果分析
      PSYoungGen(新生代):共1536k,1.5MB
      ParOldGen(老年代):共512k,0.5MB
      这些相加总内存已经等于2MB了,所以即使永久区(元空间)使用的有内存,但是它逻辑上存在但是物理上不存在

 

8.2、OOM

OutOfMemoryError:内存不足错误,说明堆内存满了

  • 解决:

    • 使用堆内存调优参数调整堆内存大小,排查是否还会出错

    • 若还会出错,使用JProfiler工具分析内存,排查具体出错位置

 

8.3、堆内存调优参数

  • -Xms1m:

    设置初始化内存分配大小,默认为内存大小的1/64

  • -Xmx8m:

    设置最大分配内存,默认为内存大小的1/4

  • -XX:+PrintGCDetails:

    打印垃圾回收的详细信息

  • -XX:+HeapDumpOnOutOfMemoryError

    Dump内存文件

  • -XX:MaxTenuringThreshold(默认15):

    设置幸存区里的对象经过多少次GC会进入老年区

  • ……

 

8.4、使用JProfiler工具分析OOM

  • 首先要设置Dump内存文件

    • 配置:-XX:+HeapDumpOnOutOfMemoryError

 

  • 执行程序在控制台找到dump内存文件的名字

 

  • 找到dump内存文件的所在位置

    双击打开

 

      打开之后如下图所示

 

  • 点击Biggest Object分析哪个对象的Retained size存在问题

 

  • 点击Thread Dump查看哪个线程在具体哪个位置出现了问题

 

9、GC

  • Garbage Collector

    垃圾回收器

  • 作用区域

    堆中的方法区和新生代,老年代

  • 分类

    • 轻GC(普通GC)

    • 重GC(全局GC)

9.1、常见算法

  • 引用计数法

    对每个对象都加上一个计数器,对象每被引用一次,计数器都加1,没被引用的对象计数器减1,垃圾回收器会回收计数器清0的对象。

 

  • 复制算法

    • 优点:没有内存碎片

    • 缺点:浪费一片内存空间

 

  • 标记清除算法

    • 优点:不需要额外的内存空间

    • 缺点:两次扫描,严重浪费时间,会产生内存碎片

 

  • 标记压缩算法

    • 优点:不会产生内存碎片

    • 缺点:要经过三次扫描,时间复杂度高

 

9.2、总结

  • 内存效率(时间复杂度):复制算法 > 标记清除算法 > 标记压缩算法

  • 内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

  • 内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

  • 分代收集算法

    • 新生代:复制算法

    • 老年代:标记清除算法 + 标记压缩算法