Java Virtual Machine

Posted 364.99°

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Virtual Machine相关的知识,希望对你有一定的参考价值。

目录

1. 概述

JVM: java程序运行环境(字节码运行环境)。

  • 好处:
    • 一处编译、到处运行
    • 自动内存管理,垃圾回收机制(GC)

2. 内存结构

1. 程序计数器

先看一段简单程序及其字节码:javap -c Demo1.class

java代码执行流程:

程序计数器:

  • 作用: 记住下一条jvm指令的地址。
    • 二进制字节码前面的数字是下一条指令的地址
    • 物理实现:寄存器(速度很快)
  • 特点:
    • 线程私有
    • 不会存在内存溢出

2. 虚拟机栈

1. 概述

栈: 先进后出的数据结构。

虚拟机栈: 线程运行时的内存空间。

  • 一个栈由多个栈帧组成,一个栈帧就对应一个方法的调用所占用的内存
  • 每个线程只有一个活动栈帧,对应着当前执行的那个方法
  • 栈帧内存在每一次方法执行完之后都会弹出栈内存

栈内存溢出原因(StackOverflowError):

  • 栈帧过多(如,不断递归调用)
  • 栈内存过大

VM options设置栈内存大小:
-Xss256k 设置每个线程的栈大小。

  • jdk5之前,每个栈的大小是 256k,之后是 1M
  • 相同物理内存下,减小此值可生成更多线程,但操作系统对于一个进程的线程数是由限制的
  • 超出线程数限制,就会报错 StackOverflowError

2. 线程诊断

Linux查看线程占用:

  • top 查看进程内存、CPU占用(定位进程)
  • ps H -eo pid,tid,%cpu | grep xxxxx 查看线程对CPU的占用(定位线程)
    • H 打印所有进程和线程
    • -eo 规定要输出的内容
      • pid,tid,%cpu pid,tid和CPU占用
    • grep 过滤进程的条件
  • jstack 进程id 列出进程中的所有线程(根据线程id—tid的十六进制数去找到目标线程)

排除线程死锁: 迟迟得不到结果。

  • jstack 进程id 会打印出死锁死锁所在范围
    Find one Java-level deadlock
    Java stack information for the threads listed above

3. 本地方法栈

本地方法栈: JVM在调用本地方法的时候需要的内存空间。

  • 本地方法:不是由Java代码编写的,可以直接与操作系统底层打交道的API(如,Object的clone()、hashCode()、notify()、wait())

4. 堆

1. 概述

堆: 通过 new ,创建对象都会使用堆内存。

  • 特点:
    • 线程共享,线程安全问题
    • 垃圾回收机制

VM options设置堆内存大小:

  • -Xmx8m 设置JVM最大堆内存为8M

2. 堆内存诊断

jps 查看有哪些java进程(显示:进程id 进程名)

jmap -heap pid 查看堆内存占用情况

jconsole 图形界面检查内存、线程、类…

jvisualvm

5. 方法区

方法区:

  • 线程共享区域
  • 存储与类结构相关的信息:run-time constant poolfieldmethod datamethodsconstructors
  • 虚拟机启动时创建
  • 逻辑上是堆的组成部分

方法区内存溢出: 1.8之前是永久代内存不足导致内存溢出、1.8之后是元空间内存不足导致内存溢出。

VM options设置永久代最大保留区域(了解即可):
-XX:MaxPermSize=2048m
在1.8已经弃用。

VM options设置元空间:

  • -XX:MetaspaceSize=100m 设置元空间初始大小为100M
  • -XX:MaxMetaspaceSize=100m 设置元空间最大可分配大小为100M

通过字节码动态生成类的包:CGLIB

1. 运行时常量池

常量池: 就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。

运行时常量池: 常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

JVM指令集

2. String Table


  • 字符串延迟加载(实例化):

    通过debug可以发现,只有当使用到某个字符串的时候,才会放入到串池(String Table)中,而且,不会重复放置相同的字符串。
    这就是延迟加载机制。

串池的位置:

  • 1.6 存在永久代中,内存溢出的时候会报错:java.lang.OutOfMemoryError: PermGen space
  • 1.8 存在堆中,内存溢出会报错:java.lang.OutOfMemoryError: Java heap spacejava.lang.OutOfMemoryError: GC overhead limit exceeded
    • 设置堆最大内存为4M:-Xmx4m

    • 代码:

      public class TestStringTableLocation 
          public static void main(String[] args) 
              List<String> list = new ArrayList<>();
              int i = 0;
              try 
                  for (int j = 0; j < 10000000; j++) 
                      list.add(String.valueOf(j).intern());
                      i++;
                  
               catch (Throwable e) 
                  e.printStackTrace();
               finally 
                  System.out.println(i);
              
          
      
      

      上述报错的原因:当98%的时间花在了垃圾回收上面,但是只有2%的堆空间被回收了,JVM就会放弃垃圾回收,直接报错(并不会报堆空间不足的错)
      如果想要报堆空间不足的错,就需要将上述的机制关掉:完整的虚拟机参数:-Xmx4m -XX:-UseGCOverheadLimit


演示串池的垃圾回收:
虚拟机参数:-Xmx4m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

  • -XX:+PrintStringTableStatistics :打印有关StringTable和SymbolTable的统计信息(+是开启、-是关闭)
  • -XX:+PrintGCDetails: 打印输出详细的GC收集日志的信息
  • -verbose:gc:在控制台输出GC情况
public class TestStringTableGC 
    public static void main(String[] args) 
        int i = 0;
        try 

         catch (Throwable e) 
            e.printStackTrace();
         finally 
            System.out.println(i);
        
    

public class TestStringTableGC 
    public static void main(String[] args) 
        int i = 0;
        try 
            for (int j = 0; j < 100; j++) 
                String.valueOf(j).intern();
                i++;
            
         catch (Throwable e) 
            e.printStackTrace();
         finally 
            System.out.println(i);
        
    

通过不断修改循环的上限值,可以从控制台看到GC回收被触发:


性能调优:
通过上述案例我们可以知道,String Table采用的是桶机制,所以:

  • 当桶足够多的时候,桶元素发生hash碰撞的几率就更小,查找速度就会增快
  • 当桶的数量较少的时候,桶元素发生hash碰撞的几率就更大,导致链表更长,从而降低查找速度

所以String Table的性能调优就是修改桶的个数。

虚拟机参数:

  • -Xms500m设置堆内存初始值为500m
  • -Xmx500m 设置堆最大内存为500M
  • -XX:+PrintStringTableStatistics 打印有关StringTable和SymbolTable的统计信息
  • -XX:StringTableSize=20000 设置串池的桶个数为20000
public class TestStringTableOptimization 
    public static void main(String[] args) throws IOException 
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\\\YH\\\\examtest20210723.sql")))) 
            String line = null;
            long start = System.nanoTime();
            while (true) 
                line = reader.readLine();
                if (line == null) 
                    break;
                
                line.intern();
            
            System.out.println("const:" + (System.nanoTime() - start) / 100000);
        
    

当桶的个数为2000时: -XX:StringTableSize=20000

当桶的个数为10000时: -XX:StringTableSize=10000

当桶的个数为1009时: -XX:StringTableSize=1009


字符串入串池的优点: 极大节约了内存占用(重复的字符串只会在串池中存储一个)

  • 不存入串池 list.add(line);

    public class TestStringTableOptimization 
        public static void main(String[] args) throws IOException 
            List<String> list = new ArrayList<>();
            System.in.read();
            for (int i = 0; i < 10; i++) 
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\\\YH\\\\examtest20210723.sql")))) 
                    String line = null;
                    long start = System.nanoTime();
                    while (true) 
                        line = reader.readLine();
                        if (line == null) 
                            break;
                        
                        /*line.intern();*/
                        list.add(line);
                    
                    System.out.println("const:" + (System.nanoTime() - start) / 100000);
                
            
            System.in.read();
        
    
    
  • 存入串池: list.add(line.intern());

3. 直接内存

直接内存:

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

不使用直接内存:

使用直接内存:

直接内存使得java代码能直接读取到系统内存的数据,极大缩减了代码执行时间。

分配直接内存缓冲区的方法:

    public static ByteBuffer allocateDirect(int capacity) 
        return new DirectByteBuffer(capacity);
    

直接内存溢出演示:

public class TestDirectOut 
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) 
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try 
            while (true) 
            	// allocateDirect分配多少内存,就会占用本地多少内存
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            
         finally 
            System.out.println(i);
                 
    


分配和回收原理:

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleanerclean 方法调用 freeMemory 来释放直接内存

JVM调优常用参数: -XX:+DisableExplicitGC

  • 让显示的垃圾回收无效(即直接手动敲代码回收,如 System.gc()
  • 因为显示的垃圾回收是一种 Full gc 即,要回收新生代,还要回收老年代,会造成较长的代码停留时间

但是,禁用掉显示的垃圾回收之后,直接内存的回收就只能依靠 Cleaner 来检测回收了,这样就会导致直接内存长时间得不到释放。

    public static void main(String[] args) throws IOException 
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    

这时候就需要使用Unsafe来手动回收内存了:

    public static void main(String[] args) throws IOException 
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    

    public static Unsafe getUnsafe() 
        try 
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
         catch (NoSuchFieldException | IllegalAccessException e) 
            throw new RuntimeException(e);
        
    

3. 垃圾回收

1. 判断对象可以被回收的算法

1. 引用计数法

引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器就减1。任何时刻计数器为0的对象就是不再被使用的。

巨大缺陷: 很难解决对象之间相互循环引用的问题,因此JVM并未采用这种方法。

2. 可达性分析算法

可达性分析算法:

  • 用过一系列的 gc root 来判断对象是否被引用
  • 如果 gc root 可以直接或间接引用到某个对象,就表明该对象被引用,反之则说明该对象不可用
  • 在下次垃圾回收到达的时候,不可用的对象就会被回收

    如图,下次垃圾回收的时候,obj9、obj10、obj11就会被回收。

GC Roots对象取用范围:

  • System Class 系统类,启动类加载器加载的类(核心类)
    • 如Object、String、HashMap等
  • Native Stack 本地方法栈的操作系统方法
  • Busy Monitorsynchronized或者lock加锁的对象
  • Thread 活动线程用到的对象(一个线程对应一个栈,栈帧内的对象)

宣告对象死亡:
宣告对象死亡至少需要经历两次标记过程:

  • 第一次标记:
    • 在对对象进行可达性分析发现对象没有被 gc root 引用,则会对其进行标记并进行第一次筛选
    • 第一次筛选主要是为了判断改对象是否需要执行finalize()
      利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期
      这是由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的
      • 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行
      • 如果对象被判定为有必要执行,则会被放到一个F-Queue队列
  • 第二次标记:
    • gc将F-Queue中的对象进行第二次标记
    • 如果这时候,对象通过调用finalize()gc root 引用链上的任何一个对象建立关联,那么此对象就会被移出即将被回收的队列

2. 五种常见引用类型

1. 简介及其回收机制

强引用:

  • 如: Object obj = new Object() 变量obj强引用了实例出的对象
  • 只要引用链能找到此对象,就不会被回收

软/弱引用:

  • 只要没有被强引用直接地引用,都有可能被垃圾回收,如下图:

    软引用被回收: 因为obj2在引用cg root引用链中没有被强引用直接引用,所以在下一次垃圾回收内存不够的时候,有可能被回收
    弱引用被回收: obj3被强引用直接引用了,所以就不会被垃圾回收。可如果obj3也没有强引用直接引用,就会在下一次垃圾回收的时候被回收

  • 当软/弱引用的对象被回收之后,如果在创建软/弱引用的时候,被分配了一个引用队列,那么软/弱引用就会进入引用队列(这两者也会占用内存,也可以释放掉)

虚/终结引用:

  • 虚/终结引用 必须配合引用队列来使用
  • 当 虚/终结引用对象 被创建的时候,就会创建一个引用队列
  • 虚引用回收:

    在创建ByteBuffer对象的时候,就会使用Cleaner来监测,而一旦没有强引用引用ByteBuffer的时候,ByteBuffer自己就会被垃圾回收掉,如下:

    但是这时候,直接内存还没有被回收,所以这时候,虚引用对象就会进入引用队列,由 Reference Handler 线程调用虚引用相关方法(Unsafe.freeMemory)释放直接内存
  • 终结引用回收:
    当没有强引用去引用对象(重写了 finallize()的对象)的时候,JVM就会给此对象创建一个终结器引用

    当对象被垃圾回收器回收的时候,终结器引用也会进入引用队列,但这时候对象还没有被回收

    然后 Finalizer 线程(此线程优先级很低,被执行的机会很少)通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

2. 代码演示

在JDK1.2之前,只有引用和没引用两种状态。

SoftReference 实现软引用的类,WeakReference 实现弱引用的类、PhantomReference 实现虚引用的类。


软引用演示:

  • 虚拟机参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc 最大堆内存20M,打印GC细节,控制台输出gc情况

  • 代码(软引用所引用对象的回收):

    • 不使用软引用情况:
      public class TestSoft 
      
          private static final int _4MB = 4 * 1024 * 1024;
          
          public static void main(String[] args) throws IOException 
              List<byte[]> list = new ArrayList<>();
              for (int i = 0; i < 5; i++) 
                  list.add(new byte[_4MB]);
              
      
              System.in.read();
          
      
      会造成内存溢出
    • 使用软引用:
      public class TestSoft 
      
          private static final int _4MB = 4 * 1024 * 1024;
      
          public static void main(String[] args) throws IOException 
              soft();
          
      
          public static void soft() 
              // list --> SoftReference --> byte[]
              List<SoftReference<byte[]>> list = new ArrayList<>();
              for (int i = 0; i < 5; i++) 
                  SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
                  System.out.println(ref.get());
                  list.add(ref);
                  System.out.println(list.size());
      
              
              System.out.println("循环结束:" + list.size());
              for (SoftReference<byte[]> ref : list) 
                  System.out.println(ref.get());
              
          
      
      
      不会造成内存溢出:

      软引用特点: 一次垃圾回收之后,内存仍然不足,就会把软引用所引用的对象回收。
  • 代码(软引用对象本身的回收