JVM(1.简单的概念)

x33g5p2x  于2022-08-17 转载在 Java  
字(9.7k)|赞(0)|评价(0)|浏览(742)

JVM

JVM内存结构

JVM内存

虚拟机栈
Java 栈

Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存

  • 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(一个方法一个栈帧

  • Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的

  • 虚拟机栈是每个线程私有的,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程

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

  • 局部变量表:存储方法里的 Java 基本数据类型以及对象的引用

  • 动态链接:也叫指向运行时常量池的方法引用

  • 方法返回地址:方法正常退出或者异常退出的定义

  • 操作数栈或表达式栈和其他一些附加信息

设置栈内存大小:-Xss size``-Xss 1024k

  • 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M

虚拟机栈特点:

  • 栈内存不需要进行GC,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据

  • 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)

  • 方法内的局部变量是否线程安全

  • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)

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

异常:

  • 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError错误
  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 错误
局部变量

局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量

  • 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
  • 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中
  • 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁
  • 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收

局部变量表最基本的存储单元是 slot(变量槽)

  • 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据
  • 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量
  • 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot
  • 局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的
操作数栈

栈:可以使用数组或者链表来实现

操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)

  • 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区
  • Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率

基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术

动态链接

动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定

  • 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用

[外链图片转存中…(img-40Zfh1tb-1658977927894)]

  • 在 Java 源文件被编译成的字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中

常量池的作用:提供一些符号和常量,便于指令的识别

[外链图片转存中…(img-yDv5SyPY-1658977927895)]

返回地址

Return Address:存放调用该方法的 PC 寄存器的值

方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置

  • 正常:调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
  • 异常:返回地址是要通过异常表来确定

正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者

异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出

两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值

附加信息

栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息

本地方法栈

本地方法栈是为虚拟机执行本地方法时提供服务的

JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植

  • 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常

  • 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一

  • 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序

  • 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限

  • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区

  • 直接从本地内存的堆中分配任意数量的内存

  • 可以直接使用本地处理器中的寄存器

图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md

程序计数器

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

作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空)

原理:

  • JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程
  • 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号

特点:

  • 是线程私有的
  • 不会存在内存溢出,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC

Java 反编译指令:

javap -v Test.class

#20:代表去 Constant pool 查看该地址的指令

0: getstatic #20 		// PrintStream out = System.out;
3: astore_1 			// --
4: aload_1 				// out.println(1);
5: iconst_1 			// --
6: invokevirtual #26 	// --
9: aload_1 				// out.println(2);
10: iconst_2 			// --
11: invokevirtual #26 	// --

Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题

存放哪些资源:

  • 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存

  • 字符串常量池:

  • 字符串常量池原本存放于方法区,JDK7 开始放置于堆中

  • 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table

  • 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中

  • 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率

设置堆内存指令:-Xmx Size

内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常

堆内存诊断工具:(控制台命令)

  1. jps:查看当前系统中有哪些 Java 进程
  2. jmap:查看堆内存占用情况 jhsdb jmap --heap --pid 进程id
  3. jconsole:图形界面的,多功能的监测工具,可以连续监测

在 Java7 中堆内会存在年轻代、老年代和方法区(永久代)

  • Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间
  • Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区
  • Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理

分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能

public static void main(String[] args) {
    // 返回Java虚拟机中的堆内存总量
    long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
    // 返回Java虚拟机使用的最大堆内存量
    long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
    
    System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
    System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M
}
方法区

方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)

方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式

方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)

方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现

为了避免方法区出现 OOM,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中

类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表

常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池

  • 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等
  • 符号引用:类、字段、方法、接口等的符号引用

运行时常量池是方法区的一部分

  • 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池
  • 类在解析阶段将这些符号引用替换成直接引用
  • 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()

本地内存

基本介绍

虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM

本地内存:又叫做堆外内存,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 Java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM

本地内存概述图:

元空间

PermGen 被元空间代替,永久代的类信息、方法、常量池等都移动到元空间区

元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制

方法区内存溢出:

  • JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space
-XX:MaxPermSize=8m		#参数设置
  • JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m	#参数设置

元空间内存溢出演示:

public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}
直接内存

直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域

直接内存详解参考:NET → NIO → 直接内存

变量位置

变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置

静态内部类和其他内部类:

  • 一个 class 文件只能对应一个 public 类型的类,这个类可以有内部类,但不会生成新的 class 文件
  • 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证)

类变量:

  • 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 Java 进程产生和销毁
  • 在 Java8 之前把静态变量存放于方法区,在 Java8 时存放在堆中的静态变量区

实例变量:

  • 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分
  • 在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中

局部变量:

  • 局部变量是定义在类的方法中的变量
  • 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,

类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?

  • 类常量池与运行时常量池都存储在方法区,而字符串常量池在 Jdk7 时就已经从方法区迁移到了 Java 堆中
  • 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符
  • 在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池
  • 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池

什么是字面量?什么是符号引用?

  • 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示
int a = 1;				//这个1便是字面量
String b = "iloveu";	//iloveu便是字面量
  • 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用它的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址
system.in.read()

线程运行诊断

cpu 占用过多

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

​ 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

堆内存诊断

  1. jps 工具
  • 查看当前系统中有哪些 java 进程
  1. jmap 工具
  • 查看堆内存占用情况 jmap - heap 进程id
  1. jconsole 工具
  • 图形界面的,多功能的监测工具,可以连续监测

方法区内存溢出

  • 1.8 以前会导致永久代内存溢出
- 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space 
-XX:MaxPermSize=8m
  • 1.8 之后会导致元空间内存溢出
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace 
* -XX:MaxMetaspaceSize=8m

StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象

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

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

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

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

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

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

GC

查看GC机制:jmap -dump:format=b,live,file=文件名.bin 进程ID

引用计数法

被引用计数+1 如果某个变量不再引用它就-1 变为0则可以被回收**缺点:**循环引用

可达性分析算法

JVM中的垃圾回收期是采用可达性分析算法来探索所有存活的对象:扫描堆中的对象看是否能够沿着GC Root对象作为起点为引用链找到该对象,若找不到则可以回收(本ROOT对象直接或间接引用的对象不可以被回收)

局部变量引用的对象可以作为根对象

方法参数可以作为跟对象

ROOT对象的分类:系统类,本地方法(操作系统的方法),运行的线程,以及锁

四种引用

强引用,软引用,弱引用,虚引用,终结器引用

1.强引用

  • 当没有强引用的时候,直接引用ROOT对象被称之为强引用不会被垃圾回收的对象

2.软引用

  • 当没有强引用的时候,如果内存充足,当发生垃圾回收的时候是不会被回收的,但当内存不足的时候发生垃圾回收的时候是会被垃圾回收的
  • 需要配合引用队列来释放软引用自身
  • 软引用对象:SoftReference

3.弱引用

  • 当发生垃圾回收的时候,一定会被垃圾回收的 (不管内存是否充足)
  • 需要配合引用队列来释放弱引用自身
  • 弱引用对象:WeakReference 例:list --> WeakReference --> byte[]

当被软引用和弱引用创建的时候分配了引用队列,那么当软弱引用引用的对象被回收时,软弱引用将会进入到该引用队列中,(优点:软弱引用也需要占用一定的内存,如果需要操作软弱引用占的内存,那就需要用引用队列来找到这两个引用)

4.虚引用

  • 需要配合引用队列使用,主要配合ByteBuffer使用,虚引用引用的的对象被垃圾回收时,虚引用对象自己就会放入引用队列,就会有间接的一个线程(Reference Handler线程)去调用虚引用相关方法直接释放内存

5.终结器引用

  • finalliz()方法,当对象重写了finalliz()方法并且当没有强引用引用它时,就可以被垃圾方法回收. 这个地方就用到终结器引用当没有强引用引用对象时,这个对象会有虚拟机来创建终结器引用,当对象被垃圾回收时,终结器引用就会进入到引用队列(这个对象还没有被垃圾回收),再由一个优先级很低的线程(Finalizer线程)会在后一时刻去查看引用队列中是否存在终结器引用,当发现有终结器引用时就会找到该对象去调用他的finalliz()方法,那么在下一次垃圾回收的时候就会回收该对象.

虚引用与终结器引用创建时必须要关联引用队列来使用

6.用引用队列配合软引用使用,进行清理

引用队列

//引用队列
RederenceQueue<byte[]> queue = new RederenceQueue<byte[]>;
//关联了引用队列,当软引用所关联的byte[]被回收时,软引用会自己加入到queue中
SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[1024*4*1024],queue);
Reference<? extends byte[]> poll = queue.poll();
while(poll != null){
    list.remove(poll);
    poll = queue.poll();
}

垃圾回收算法

1.标记清除算法

  • 工作方式:先标记然后进行垃圾回收的时候再清除,清除不是直接清零处理,是把这个这个对象起始结束的地址记录下来,放在空闲的地址列表里,下载分配新的对象的时候就会在地址列表里去找,看有没有足够的地址空间结束新的对象,如果有就会进行内存分配,并不会对占用的内存进行清零的处理

优点:垃圾回收的速度快 缺点:空间不连续,会产生内存碎片

2.标记整理算法

  • 在垃圾清理的时候,可用的内存碎片要移动,使内存更为紧凑一点
  • 避免标记清除算法的空间不连续产生内存碎片的问题
  • **缺点:**对象需要移动,效率会比较低

3.复制算法

  • 缺点:会占用双倍的内存空间

分代回收

三种垃圾回收算法,视情况而定---------------------------------->分代回收

  • 新生代 ---- > 垃圾回收执行的比较频繁 Minor GC

  • 伊甸园

  • 幸存区From

  • 幸存区 To

  • 老年代 — >垃圾回收执行的频率比较低 Full GC

  • 对象首先分配在伊甸园区域

  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象copy到to中,存活的对象年龄也要加1,并且交换from to

  • minor gc 会引发 stop the world(STW暂停其他用户线程),等垃圾回收结束,用户线程才恢复运行

  • 当对象寿命超过阈值时,会晋升值老年代,最大寿命是15(4bit)

  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc(STW的时间更长)如果full gc执行后空间还是不足的话就会报:内存溢出 java.lang.OutOfMemoryError: PermGen space

----------------------->分代回收

  • 新生代 ---- > 垃圾回收执行的比较频繁 Minor GC

  • 伊甸园

  • 幸存区From

  • 幸存区 To

  • 老年代 — >垃圾回收执行的频率比较低 Full GC

  • 对象首先分配在伊甸园区域

  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象copy到to中,存活的对象年龄也要加1,并且交换from to

  • minor gc 会引发 stop the world(STW暂停其他用户线程),等垃圾回收结束,用户线程才恢复运行

  • 当对象寿命超过阈值时,会晋升值老年代,最大寿命是15(4bit)

  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc(STW的时间更长)如果full gc执行后空间还是不足的话就会报:内存溢出 java.lang.OutOfMemoryError: PermGen space

对于OopMap与安全点和安全区域与RememberedSet 理解请看我的这篇Blog:
OopMap与安全点和安全区域与RememberedSet 理解

相关文章