JVM-02.JVM内存结构

思维导图:点击查看思维导图.

一、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常用参数设置
在这里插入图片描述
设置方法: tomcat设置参数需要在tomcat/bin目录下的catalina.sh中添加jvm的参数。springboot 在启动jar的时候添加jvm参数即可。

 java ‐Xms2048MXmx2048MXmn1024MXss512K ‐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内存模型图

在这里插入图片描述
JAVA类加载链接: 点击进入JVM-01.JVM类加载机制.

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)问题?
可能原因:

  1. 栈帧过大导致栈内存溢出(一般不会出现)
  2. 栈帧过多导致栈内存溢出(举例:方法递归调用,循环引用)

案例:

  1. CPU占用过多
    定位:
    a. 用top命令定位哪个线程对CPU占用过高
    b. ps H -eo pid,tid,%cpu |grep 进程id(用ps命令定位哪一个线程CPU占用过高)
    c. jstack 进程id 命令, 可以根据线程ID找到有问题的线程,进一步定位到问题代码的源码行数
  2. 程序运行长时间没有结果
    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)
  • 堆内存诊断 :使用下列工具进行查看
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进程
在这里插入图片描述

2.jmap工具

作用: 查看堆内存占用情况(jmap -heap +进程id)
在这里插入图片描述
在这里插入图片描述

3.jconsole工具
  • Terminal输入jconsole会出现图形化工具,选择进程连接进入查看
  • 连续的堆内存检测,非常直观,可以手动执行GC,可以查看内存,类,进程等,还可以检测死锁等
    在这里插入图片描述
4.jvisualvm工具
  • List itemTerminal输入jvisualvm,连接到指定的进程。功能非常强大
  • 堆dump(堆转储),抓取堆当前快照,收集堆有哪些对象及对象个数等,占用堆大小,检查可以进行查找最大的对象前n个,可用于堆内存分析,解决生成问题等
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

V.方法区(Method Area)

定义: 所有JVM线程共享的区,存储类相关信息(成员变量【实例变量+类变量,存放在堆中,和类一起被创建】,运行时常量池,方法数据,成员方法及构造器方法的代码部分,特殊方法等)
特点: 虚拟机启动时被创建,逻辑上是堆的组成部分(具体实现不同JVM厂商实现方式不同,不同实现方法区位置不同,注意方法区也会抛出OutOfMemoryError

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

结构图:
在这里插入图片描述
方法区内存溢出问题(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. 1.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.如果程序中含有大量字符串,使用Inintern()进行字符串入池 ,减少字符串个数,节约堆内存的使用

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

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
	}
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值