2.JVM内存结构

225 阅读14分钟

思维导图:点击查看思维导图
文章图片:点击查看图片

一、JVM是什么?

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

特点:

  • 一次编写,到处运行 (JVM运行编译后的字节码文件屏蔽了底层操作系统和JAVA代码之间的差异)
  • 自动内存管理机制,垃圾自动回收
  • 数组下标越界检查
  • 多态
  • ...

JVM、JRE、JDK之间的关系

  • JVM
  • JRE(JVM + 基础类库)
  • JDK(JVM + 基础类库 + 编译工具)
  • JavaSE(JDK + IDE工具)
  • JavaEE (JDK + IDE工具+应用服务器)

二、JVM参数设置

-XX、-X、-version:X越多代表越不稳定,X越少越稳定(不是性能不稳定,而是升级迭代时不能用或过时)

JVM常用参数设置

image.png

设置方法

  • Tomcat 设置参数需要在 tomcat/bin 目录下的 catalina.sh 中添加 jvm 的参数。
  • SpringBoot 在启动 jar 的时候添加 JVM参数即可。
java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar ./target/xxx.jar

  • -Xss  size 为栈内存指定大小,不指定默认。不是栈内存越大越好,一般采用默认
  • 指定1M栈内存的方式:-Xss 1m 或 -Xss 1024k 或 -Xss 1048576

  • -Xms2048m:设置堆内存大小,建议与 Xmx 相同,避免每次垃圾回收完后 JVM 重新分配内存
  • -Xmx2048m:设置 JVM 最大可用内存为 2048M
  • -Xmn2g:设置新生代大小为 2G

方法区

  • -XX:MaxMetaspaceSize: 设置元空间最大值,默认是 -1,即不限制(取决于本地内存大小)
  • -XX:MetaspaceSize: 指定元空间触发 FullGC 的初始阈值(元空间无固定初始大小),默认是 21M,达到该值就会触发 Full GC, 同时 JVM 会对该值进行动态调整。 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过 -XX:MaxMetaspaceSize 的情况下, 适当提高该值。
  • 初始元空间和最大元空间,一般设置为一样。生产环境一定要设置,否则频繁 Full GC。并设置得比初始值要大,对于 8G 物理内存的机器来说,一般将这两个值都设置为 256M。

三、JVM内存模型

1.JAVA内存模型图

image.png

2.内存结构

I.程序计数器(Program Counter Register)

本质: 程序计数器本质是通过寄存器来实现(寄存器读取速度最快)

JAVA代码运行流程: Java 源代码 -> 字节码 -> JVM 指令 -> 解释器 -> 机器码 -> CPU

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

特点:

  • 线程私有
  • 唯一一个没有内存溢出的区
  • 程序每运行一步,字节码执行引擎都会进行修改
  • 多线程中因为某种原因终止,再次执行时也通过程序计数器存储上一次运行的位置

II.虚拟机栈(Java Virtual Machine Stacks)

栈: 先进后出的数据结构

定义: 线程运行时需要的内存空间

组成: 由多个栈帧组成(Debuger 模式下的 Frames 可以查看到栈帧及栈内存 [Variables])

  • 栈帧(FRAME): 每个方法运行时需要的内存(参数、局部变量、返回地址),随着方法的调用而创建,随着方法的结束而销毁(无论是否正常结束)
  • 活动栈帧: 栈顶部的栈帧,每个线程只能有一个活动栈帧,对应着正在执行的那个方法
  • 栈与栈帧的关系: 方法执行栈帧入栈,方法运行完栈帧出栈,方法中调用方法,栈中就有多个栈帧,符合栈的数据结构

栈与堆的关系: 堆存放对象,栈存储着对象在堆中的内存地址

栈存储的内容: 方法内的部变量表、操作数、动态链接、方法出口信息、其他等信息。

  • 局部变量表: 作用是保存函数的参数以及局部变量 (存在于栈内存中,当方法执行完成,让出内存,让其他方法来使用内存,例如:参数a = 1) ,函数调用结束随栈帧销毁
  • 操作数栈: 主要用于保存计算过程的中间结果,存储计算过程中的临时变量 (例如:a = 1; b = 2; c =(a + b)5;执行 (a + b) 5时会将a,b的值从局部变量表拿到操作数栈再从操作数栈中拿出去相加,将结果3再压入操作数栈,然后执行乘法时将5压入操作数栈,再将5和3弹出计算结果15压入操作数栈,最后从操作数栈将15出栈赋给局部变量c),可以从class文件的java -p 通过JVM指令分析
  • 动态链接: 栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用(地址:直接引用,程序运行过程中,将符号引用转换为内存中的对应地址)
  • 方法出口信息: 在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来恢复它的上层方法的执行状态,方法出口信息获取分为正常退出和异常退出。正常退出通过 PC计数器的值获取,异常退出通过异常处理器表确定返回地址。

几个问题:

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

  • 栈内存不会也不需要进行垃圾回收的处理

b. 局部变量是否存在线程安全问题?

  • 存在,栈是线程独有的,但存在逃逸现象。
  • 逃逸分析,如果方法类局部变量方法未逃离方法作用的范围,它是线程安全的,如果是局部变量引用了对象,并逃离里方法作用的范围,需要考虑线程安全问题
// 线程安全
public static  void test1() {
    StringBuilder sb = new StringBuilder();
    sb.append("a");
    sb.append("b");
    System.out.println(sb.toString());
}
// 线程不安全,引用了外部StringBuilder
public static  void test2(StringBuilder sb) {
    sb.append("a");
    sb.append("b");
    System.out.println(sb.toString());
}
// 线程不安全,返回了一个引用,其他对象可能拿到该引用去修改它
public static  StringBuilder test3() {
    StringBuilder sb = new StringBuilder();
    sb.append("a");
    sb.append("b");
    return sb;
}

c. 栈内存溢出(java.lang.StackOverflowError)问题?

  • 栈帧过大导致栈内存溢出(一般不会出现)
  • 栈帧过多导致栈内存溢出(方法递归调用,循环引用等)

案例:

CPU 占用过多怎么定位

  • a. 用 top 命令定位哪个线程对 CPU 占用过高
  • b. ps H -eo pid,tid,%cpu |grep 进程id(用ps命令定位哪一个线程CPU占用过高)
  • c. jstack 进程 id 命令, 可以根据线程 ID 找到有问题的线程,进一步定位到问题代码的源码行数

程序运行长时间没有结果 

  • a.可以用 jstack 进行分析,报告问题:Found one Java-level deadlock
  • b.死锁(互斥,占有且等待,不可抢占,循环等待)

III.本地方法栈(Native Method Stacks)

作用: 给本地方法运行提供内存空间

特点: 本地方法被 native 修饰,没有实现

举例: Object 类的 clone() 方法、wait() 方法等

IV.堆 (Heap)

特点:

  • 通过 new 关键字,创建对象会使用堆内存(大部分,可能是栈上分配)
  • 线程共享的内存区域,要考虑线程安全问题
  • 垃圾回收机制

堆内存溢出(java.lang.OutOfMemoryError: Java heap space)

  • 堆内存设置(-Xmx8m)
  • 堆内存诊断 :使用下列工具进行查看,生成上一般都有专业的监控工具,例如阿里的 arthas 阿尔萨斯
public static void main(String[] args) throws InterruptedException {
    System.out.println("Start 1...");
    Thread.sleep(20000);
    byte [] array = new byte[1024*1024*10]; // 10Mb
    System.out.println("Start 2...");
    Thread.sleep(20000);
    array = null; // 垃圾回收
    System.gc(); 
    System.out.println("Start 3...");
    Thread.sleep(20000);
}

1.jps

作用: 查看当前系统中有哪些JAVA进程

image.png

2.jmap

作用: 查看堆内存占用情况(jmap -heap +进程id),生成环境慎用,需要输出堆内存快照,会暂停用户程序!

image.png

image.png

3.jconsole工具

  • Terminal 输入 jconsole 会出现图形化工具,选择进程连接进入查看
  • 连续的堆内存检测,非常直观,可以手动执行 GC,可以查看内存,类,进程等,还可以检测死锁等

image.png

4.jvisualvm工具

  • List itemTerminal 输入 jvisualvm,连接到指定的进程。功能非常强大
  • 堆 dump(堆转储),抓取堆当前快照,收集堆有哪些对象及对象个数等,占用堆大小,可以查找最大的对象前 n 个,可用于堆内存分析,解决生产问题等

image.png

image.png

V.方法区(Method Area)

定义: 所有 JVM 线程共享的区,存储类相关信息(成员变量【实例变量+类变量,存放在堆中,和类一起被创建】,运行时常量池,方法数据,成员方法及构造器方法的代码部分,特殊方法等)

特点: 虚拟机启动时被创建,逻辑上是堆的组成部分(具体实现不同 JVM 厂商实现方式不同,不同实现方法区位置不同,注意方法区也会抛出 OutOfMemoryError

方法区与堆间的关系:方法区中如果静态变量的值是对象,那么存放的是该对象在堆中的地址

结构图:

image.png 方法区内存溢出问题(java.lang.OutOfMemoryError: Metaspace / PermGen space):

设置永久代内存大小:-XX:MaxPermSize=8m

  • JDK<1.8 可能会导致永久代内存溢出

设置元空间内存大小:-XX:MaxMetaspaceSize=8m 元空间并不在虚拟机中,而采用本地内存,不设置仅取决于本地内存

  • JDK>1.8 可能会导致元空间内存溢出

溢出场景(动态加载类,导致方法区内存溢出:使用框架不合理)

  • Spring框架
  • Mybatis框架

Class常量池、常量池与运行时常量池

javap -v xx.class查看反编译的class文件,可以查看里面的结构及运行顺序


Class常量池: Class文件中的资源仓库,含类的版本、字段、方法、接口等描述信息,还包含常量池

常量池: 主要用于存放编译期生成的各种字面量符号引用,维护的一张常量表,虚拟机执行根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。程序未运行就存在,class文件里面的16进制 。红框标出的就是class常量池信息。

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

字面量:由字母、数字等构成的字符串或者数值常量。 字面量只可以右值出现,所谓右值是指等号右边的值,如:int a = 1 这里的 a 为左值,1 为右值。例子中的 a,b 都是字面量。

int a = 1;
int b = "bbbb";

符号引用

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用动态链接例如,test() 方法运行时,test()这个符号引用就会转换为方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。

StringTable 串池(字符串常量池)

  1. 常量池中的字符串仅是符号,第一次使用时才会变为对象
  2. 利用串池的机制,可以避免重复的创建字符串对象(串池中字符串对象不可重复)
  3. 字符串变量拼接原理StringBuild + toString (JDK1.8)
  4. 字符串常量拼接原理是编译器优化
  5. 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池(串池中有不放入,无,放入并返回)
  6. new String("a") 创建的对象在堆中,但串池中也会有"a"对象
  7. JDK1.6 串池在永久代,1.8串池在堆空间

注意: inner JDK1.8不会拷贝一份放入串池,JDK1.6 会拷贝一份,所以 System.out.println(m7 == "cd");是 false  JDK1.8 设置 -Xmx10m -XX: -UseGCOverheadLimit 设置堆空间大小,关闭垃圾回收堆内存限制(+打开)。JDK1.6设置-XX:MaxPermSize=10m 永久代大小

package com.zhe.jvm;
// StringTable["a","b","ab"] hashTable结构,不能扩容,惰性添加,遇到了才加入
public class Test {
    public static void main(String[] args) {
        // 常量池中的信息,都会被加载到运行时常量池中,这时a b ab都是常量池中的符号,还不是java字符串对象
        // 当用到时才会转换为字符串对象,串池中没有,创建,有则使用
        // jvm指令ldc #2 会把 a 符号变为 "a" 字符串对象
        // jvm指令ldc #3 会把 b 符号变为 "b" 字符串对象
        // jvm指令ldc #4 会把 ab 符号变为 "ab" 字符串对象
        String m1 = "a";
        String m2 = "b";
        String m3 = "ab";
        // javac 在编译期的优化,在编译期间已经确定为"ab";
        // ldc #4 直接在常量池中找值为ab的符号
        String m4 = "a" + "b"; 
        //m5运行过程 
        //new StringBuilder().append("a").append("b").toString;
        //toString ——>new String("ab");
        String m5 = m1 + m2;
        String m6 = new String("c") + new String("d");
        String m7 = m6.intern();//主动将串池中还没有的字符串对象放入串池(串池中有不放入,无,放入并返回)
        System.out.println(m3 == m4); // true 常量池中 常量池中
        System.out.println(m3 == m5); // false  常量池中 堆中
        System.out.println(m4 == m5); // false  常量池中 堆中
        System.out.println(m7 == "cd");// true JDK1.8 false JDK1.6
    }
}

三种字符串操作(Jdk1.7 及以上版本)

1.直接赋值

// JVM 先去串池通过equals(key)方法判断是否有相同对象,如果有
// 直接返回对象在串池中的引用,没有在串池中创建一个对象并返回
 String s = "hello"; // s 指向串池中的引用

2.new String()

// 先检查hello这个字面量在串池中是否存在
// 不存在则先在串池中创建对象,再在内存中创建一个字符串对象
// 存在则直接在堆内存中创建一个字符串对象,并返回一个引用
String s1 = new String("hello"); // s1指向内存中的对象引用

3.intern方法

String s1 = new String("hello");
String s2 = s1.intern();
System.out.println(s1 == s2); //false

String中的intern方法是一个 native 的方法,当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串 (用 equals(oject) 方法确定),则返回池中的字符串。否则,将 intern 返回的引用指向当前字符串 s1(jdk1.6版本会将 s1 复制到字符串常量池里)。

StringTable性能调优

StringTable底层: 类似HashTable,是 hash 表,性能与 buckets 桶个数密切相关,桶越多元素越分散,链表越短,哈希碰撞越少,查找速度越快 Java 运行时类名,方法名,常量等也是以字符串形式存储在串池中

StringTable也会进行垃圾回收

StringTable性能调优

a.调整HashTable桶个数 每往hash表里放一个数据就要去查询一次有没有该串-XX: +PrintStringTableStatistics 可以在运行时控制台打印垃圾回收与StringTable相关内容 -XX: StringTableSIze=20000 调整StringTable桶个数

b.如果程序中含有大量字符串,使用 intern() 进行字符串入池 ,减少字符串个数,节约堆内存的使用

八种基本类型的包装类和对象池

java 中基本类型的包装类Byte,Short,Integer,Long,Character,Boolean都实现了常量池技术(严格来说应该叫对象池,在堆上)。 Byte,Short,Integer,Long,Character 这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,即对象不负责创建和管理大于127 的这些类的对象。因为一般这种比较小的数用到的概率相对较大。

public class Test { 
    public static void main(String[] args) {
        //在值[-128, 127]时可以使用对象池
        // range [-128, 127] must be interned (JLS7 5.1.7)
        Integer i1 = 127; //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池
        Integer i2 = 127;
        System.out.println(i1 == i2);//输出true
        
        //值大于127时,不会从对象池中取对象
        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4);//输出false
        
        //用new关键词新生成对象不会使用对象池
        Integer i5 = new Integer(127);
        Integer i6 = new Integer(127);
        System.out.println(i5 == i6);//输出false
​
        //Boolean类也实现了对象池技术
        Boolean bool1 = true;
        Boolean bool2 = true;
        System.out.println(bool1 == bool2);//输出true
​
        //浮点类型的包装类没有实现对象池技术
        Double d1 = 1.0;
        Double d2 = 1.0;
        System.out.println(d1 == d2);//输出false
    }
}