JVM-内存结构

引言

什么是JVM?

定义:

Java Virtual Machine - java 程序的运行环境 (Java二进制字节码的运行环境)

好处:

  • 一次编写,到处运行

  • 自动内存管理,垃圾回收功能

  • 数组下标越界检查

  • 多态

比较: jvm jre jdk

JVM-内存结构_第1张图片

学习JVM有什么用?

  • 面试

  • 理解底层的实现原理

  • 中高级程序员必备技能

常见的JVM

学习路线

JVM-内存结构_第2张图片

内存结构

程序计数器

JVM-内存结构_第3张图片

定义:Program Counter Register 程序计数器(寄存器)

作用:是记住下一条jvm指令的执行地址

JVM-内存结构_第4张图片

上图:

  • 解释器会解释指令为机器码交给CPU执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行

  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录下一行指令的地址行号,以便于接着往下执行。

特点:

  • 是线程私有的

  • 不会存在内存溢出

虚拟机栈

栈 - 线程运行需要的内存空间

栈帧 - 每个方法运行时需要的内存

定义:

  • 每个线程运行时所需要的内存,称为虚拟机栈

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧(栈顶),对应着当前正在执行的那个方法

线程安全

问题辨析

1.垃圾回收是否涉及栈内存?

  • 不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

2.占内存分配越大越好吗?

  • 不是。因为物理内存是一定的,占内存越大,可以支持更多的递归调用,但是可执行的线程数就越少。

3.方法内的局部变量是否线程安全?

  • 如果方法内局部变量没有逃离方法的作用访问,它是安全的

  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

内存溢出

栈帧过多导致内存溢出

栈帧过大导致内存溢出

  • 栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小

线程判断

案例一:CPU占用过多

  • 解决方法:Liunx环境下运行某些程序时,可能会导致CPU的占用过高,这时就需要定位占用CPU过高的线程。

定位:

  • 用top定位哪个进程对CPU的占用过高

  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)

  • jstack 进程id ,可以根据线程id找到有问题的线程,进一步定位到有问题代码的源码行号【注意 jstack 查找出的线程 id 是 16 进制的,需要转换】

案例二:程序运行很长时间没有结果

本地方法栈

一些带有native关键字的方法就是需要Java去调用本地的C或者C++方法,因为Java有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带native关键字的方法。

Heap 堆

  • 通过new关键字,创建对象都会使用堆内存

特点:

  • 他是线程共享的,堆中对象都需要考虑线程安全的问题

  • 有垃圾回收机制

堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出 可以使用 -Xmx8m 来指定堆内存大小。

堆内存诊断

1.jps工具

  • 查看当前系统中有哪些Java进程

2.jmap工具

  • 查看堆内存占用情况 jmap -heap 进程id

3.jconsole工具

  • 图形界面的,多功能的检测工具,可以连续监测

4.jvisualvm工具

方法区

定义

Java虚拟机有一个在所有Java虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据、以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区,则可以收缩。方法区域的内存不需要是连续的。

组成

Hotspot 虚拟机 jdk1.6 1.7 1.8 内存结构图

JVM-内存结构_第5张图片

方法区内存溢出
  • 1.8 之前会导致永久代内存溢出

    • 使用 -XX:MaxPermSize=8m 指定永久代内存大小

  • 1.8 之后会导致元空间内存溢出

    • 使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

运行时常量池

二进制字节码包含【类的基本信息、常量池、类方法定义,包含了虚拟机的指令】

public class Test {
 ​
     public static void main(String[] args) {
         System.out.println("Hello World!");
     }
 ​
 }

然后使用 javap -v Test.class 命令反编译查看结果。

JVM-内存结构_第6张图片

每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。

JVM-内存结构_第7张图片

常量池:

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

运行时常量池:

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

StringTable

  • 常量池中的字符串仅是符号,只有在被用到的时候才会转化为对象【延迟实例化】

  • 利用串池的机制,来避免重复创建字符串对象

  • 字符串变量拼接的原理是StringBuilder

  • 字符串常量拼接的原理是编译器优化

  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入。如果没有则放入串池,会把串池中的对象返回

    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有就会把此对象复制一份,放入串池,会把串池中的对象返回。

//StringTable ["a","b","ab"] hashtable 结构,不能扩容
 public class Demo1_22{
     //常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 Java 字符串对象
     //ldc #2 会把 a 符号 变为 "a" 字符串对象
     //ldc #3 会把 b 符号 变为 "b" 字符串对象
     //ldc #4 会把 ab 符号 变为 "ab" 字符串对象
     
     public static void main(String[] args){
         String s1 = "a";    //懒惰的 【懒加载?】
         String s2 = "b";
         String s3 = "ab";
         String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()      new String("ab")
         String s5 = "a" + "b"; //javac 在编译期间的优化,结果在编译器确定为ab
         
         System.out.println(s3 == s5)''
     }
 }

StringTable 的位置

jdk 1.6 StringTable 位置是在永久代中的,1.8 StringTable 位置是在堆中的。

JVM-内存结构_第8张图片

StringTable 垃圾回收

-Xmx10m 指定堆内存大小 -XX:+PrintStringTableStatistics 打印字符串常量池信息 -XX:+PrintGCDetails -verbose:gc 打印 gc 的次数,耗费时间等信息

 
  
/**
  * 演示 StringTable 垃圾回收
  * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
  */
 public class Code_05_StringTableTest {
 ​
     public static void main(String[] args) {
         int i = 0;
         try {
             for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
                 String.valueOf(j).intern();
                 i++;
             }
         }catch (Exception e) {
             e.printStackTrace();
         }finally {
             System.out.println(i);
         }
     }
 ​
 }
 
  

StringTable 性能调优

因为StringTable 是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池中所需要的时间

-XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑是否需要将字符串对象入池 可以通过 intern 方法减少重复入池

直接内存

定义

Direct Memory

  • 常见于NIO操作时,用于数据缓冲区

  • 分配回收成本较高,但读写性能高

  • 不受JVM内存回收管理

使用直接内存的好处

文件读写流程

JVM-内存结构_第9张图片

因为Java不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区,然后将系统缓冲区数据,复制到Java 的堆内存中。缺点就是数据存储了两份,在系统内存中有一份,Java 堆内存中有一份,造成了不必要的复制。

使用DirectBuffer文件读取流程

JVM-内存结构_第10张图片

直接内存是操作系统中和Java代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

堆内存回收原理

 
public class Code_06_DirectMemoryTest {
 ​
     public static int _1GB = 1024 * 1024 * 1024;
 ​
     public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
 //        method();
         method1();
     }
 ​
     // 演示 直接内存 是被 unsafe 创建与回收
     private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
 ​
         Field field = Unsafe.class.getDeclaredField("theUnsafe");
         field.setAccessible(true);
         Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
 ​
         long base = unsafe.allocateMemory(_1GB);
         unsafe.setMemory(base,_1GB, (byte)0);
         System.in.read();
 ​
         unsafe.freeMemory(base);
         System.in.read();
     }
 ​
     // 演示 直接内存被 释放
     private static void method() throws IOException {
         ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
         System.out.println("分配完毕");
         System.in.read();
         System.out.println("开始释放");
         byteBuffer = null;
         System.gc(); // 手动 gc
         System.in.read();
     }
 ​
 }

直接内存的回收不是通过JVM 的垃圾回收释放的,而是通过unsafe.freeMemory 来手动释放。

第一步:allocateDirect 的实现

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

底层是创建了一个DirectByteBuffer 类

第二步:DirectByBuffer 类

 DirectByteBuffer(int cap) {   // package-private
    
     super(-1, 0, cap, cap);
     boolean pa = VM.isDirectMemoryPageAligned();
     int ps = Bits.pageSize();
     long size = Math.max(1L, (long)cap + (pa ? ps : 0));
     Bits.reserveMemory(size, cap);
 ​
     long base = 0;
     try {
         base = unsafe.allocateMemory(size); // 申请内存
     } catch (OutOfMemoryError x) {
         Bits.unreserveMemory(size, cap);
         throw x;
     }
     unsafe.setMemory(base, size, (byte) 0);
     if (pa && (base % ps != 0)) {
         // Round up to page boundary
         address = base + ps - (base & (ps - 1));
     } else {
         address = base;
     }
     cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
     att = null;
 }

这里调用了一个Ckeaner 的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象【这里是DirectByteBuffer】被回收之后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

  
public void clean() {
         if (remove(this)) {
             try {
             // 都用函数的 run 方法, 释放内存
                 this.thunk.run();
             } catch (final Throwable var2) {
                 AccessController.doPrivileged(new PrivilegedAction() {
                     public Void run() {
                         if (System.err != null) {
                             (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                         }
 ​
                         System.exit(1);
                         return null;
                     }
                 });
             }
 ​
         }
     }

可以看到关键的一行代码,this.thunk.run() ,thunk 是 Runnable 对象。run 方法就是回调Deallocator中的 run 方法。

		public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            // 释放内存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

直接内存的回收机制总结

  1. 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用 freeMemory 方法

  2. ByteBuffer 的实现内部使用了 Cleaner(虚引用) 来检测 ByteBuffer。一旦 ByteBuffer 被垃圾回收,那么会由ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存。

注意:

 
/**
      * -XX:+DisableExplicitGC 显示的
      */
     private static void method() throws IOException {
         ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
         System.out.println("分配完毕");
         System.in.read();
         System.out.println("开始释放");
         byteBuffer = null;
         System.gc(); // 手动 gc 失效
         System.in.read();
     }

一般用JVM调优是,会加上下面的参数:

 -XX:+DisableExplicitGC  // 静止显示的 GC

意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。

该创作部分来自@CodeAli 

你可能感兴趣的:(JVM,jvm,开发语言,java)