【JVM】Java内存模型

1. java虚拟机的生命周期

生命周期起点是当一个java应用main函数启动时虚拟机也同时被启动,而只有当在虚拟机实例中的所有非守护进程都结束时,java虚拟机实例才结束生命。

2. java虚拟机与main方法的关系

main函数就是一个java应用的入口,main函数被执行时,java虚拟机就启动了。启动了几个main函数就启动了几个java应用,同时也启动了几个java的虚拟机。

3. java的虚拟机种有两种线程

一种叫守护线程,一种叫非守护线程(也叫普通线程),main函数就是个非守护线程,虚拟机的gc就是一个守护线程。java的虚拟机中,只要有任何非守护线程还没有结束,java虚拟机的实例都不会退出,所以即使main函数这个非守护线程退出,但是由于在main函数中启动的匿名线程也是非守护线程,它还没有结束,所以jvm没办法退出

public class MianAndThread{
    public static void main( String args[]){
        new Thread(new Runnable(){
                    @override
                    public void run(){
                        Thread.currendThread.sleep(5000s);
                        System.out.println("睡了5s后打印,这是出main之外的非守护线程,这个推出后这个引用结束,jvm声明周期结束。任务管理的java/javaw.exe进程结束"
                    }
        }
        System.out.println("mian线程直接打印,mian线程结束,电脑任务管理器的java/javaw.exe进程并没有结束。")
    }   
} 

4. JVM内存分哪几个区,每个区的作用是什么?

在这里插入图片描述

4.1 方法区(线程共享)

4.1.1 方法区中存放数据

类元信息(类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表)
常量池表存储了类在编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中

运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些
运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的intern()方法

4.1.2 GC情况

在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载

4.2 堆(线程共享)

在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此java堆的空间也是最大的。如果java堆空间不足了,程序会抛出OutOfMemoryError异常。
该区域经常发生垃圾回收操作。

4.2.1 堆中存放数据

  1. 对象实例
  2. 字符串常量池 (存储的是string对象的直接引用,而不是直接存放的对象,是一张string table)
  3. 静态变量(静态变量是有static修饰的变量)
  4. 线程分配缓冲区(Thread Local Allocation Buffer)
    线程私有,但是不影响java堆的共性,增加线程分配缓冲区是为了提升对象分配时的效率

4.2.1 堆内存结构

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
新生代内存(Young Generation)
老生代(Old Generation)
永久代(Permanent Generation)
下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。
在这里插入图片描述
JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存。

4.3 栈

在这里插入图片描述
虚拟机栈是线程私有的,随线程生灭。虚拟机栈描述的是线程中的方法的内存模型:

每个方法被执行的时候,都会在虚拟机栈中同步创建一个栈帧。方法被执行时入栈,执行完后出栈。

每个栈帧的包含如下的内容

  1. 局部变量表 – 方法内局部变量的引用
  2. 操作数栈 – 用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
  3. 动态连接 – 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
  4. 方法返回地址 —

虚拟机栈可能会抛出两种异常:
如果线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出StackOverFlowError即栈溢出(递归很深就会造成栈溢出)
如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出

4.4 本地方法栈

本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。

4.5 程序计数器

在任何时刻,一个处理器内核只能运行一个线程,多线程是通过线程轮流切换,分配时间来完成的,这就需要有一个标志来记住每个线程执行到了哪里,这里便需要到了程序计数器。
所以,程序计数器是线程私有的,每个线程都已自己的程序计数器。
程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

4.6 方法区与堆的区别

方法区存放了类的信息,有类的静态变量、final类型变量、field自动信息、方法信息,处理逻辑的指令集,我们仔细想想一个类里面也就这些东西,而堆中存放是对象和数组,咋一看好像方法区跟堆的作用是一样的。其实呢,1,这里就关系到我们平时说的对象是类的实例,是不是有点恍然大悟了?这里的对应关系就是 “方法区–类” “堆–对象”,以“人”为例就是,堆里面放的是你这个“实实在在的人,有血有肉的”,而方法区中存放的是描述你的文字信息,如“你的名字,身高,体重,还有你的行为,如吃饭,走路等”。2,再者我们从另一个角度理解,就是从前我们得知方法区中的类是唯一的,同步的。但是我们在代码中往往同一个类会new几次,也就是有多个实例,既然有多个实例,那么在堆中就会分配多个实例空间内存。

5. jvm各模块的生命周期总结

*启动一个jvm虚拟机程序就是启动了一个进程。启动的同时就在操作系统的堆内存中开辟一块jvm内存区
虚拟机栈、本地方法栈、程序计数器这三个模块是线程私有的,有多少线程就有多少个这三个模块,声明周期跟所属线程的声明周期一致。以程序计数器为例,因为多线程是通过线程轮流切换和分配执行时间来实现,所以当线程切回到正确执行位置,每个线程都有独立的程序技术器,各个线程之间的计数器互不影响,独立存储。
其余是跟JVM虚拟机的生命周期一致。

方法区的内存空间不能满足内存分配需要时,将抛出OutOfMemoryError异常
java栈空间不足了,程序会抛出StackOverflowError异常
java堆空间不足了,程序会抛出OutOfMemoryError异常
程序计数器模块是JVM内存区域唯一不会报outofMemoryError情况的区域

6. 面试题 类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?

类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中。

在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;

对于文本字符来说,它们会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池;字符串常量池存储的是字符串对象的引用,而不是字符串本身。

参考:https://zhuanlan.zhihu.com/p/428694393

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值