JVM面试_4

关于java内存模型我还没怎么整明白,我先写着,大家先看看jvm内存模型的面试题先
java内存模型简介

内存简介: 计算机所有程序都是在内存中运行的.

在程序执行的过程中,需要不断地将内存的逻辑地址和物理地址进行映射,找到相关的指令和数据去执行.
在这里插入图片描述
32位处理器: 2^32的可寻址范围,即4G.
64位处理器: 2^64的可寻址范围.

内存中地址空间的划分

  1. 内核空间
  2. 用户空间(java程序使用的就是这个)
    在这里插入图片描述
    emm,我在这里先粗暴的认为java内存模型包括用户空间内核空间,而用户空间就包括运行时数据区(jvm内存模型)

JVM内存模型–JDK8下

java程序运行在虚拟机之上,运行时需要内存空间,虚拟机执行java程序的过程中,会把它管理的内存划分为不同的数据区域.

从线程角度看:

在这里插入图片描述
线程私有: 程序计数器,虚拟机栈,本地方法栈.
线程共享: MetaSpace(元空间,可以就理解为方法区,方法区是一种规范,而元空间是一种实现), java堆.


java内存模型之线程独占部分

程序计数器

  • 它是一块较小的内存空间,它的作用: 可以看作是当前线程执行的字节码文件的行号指示器,它是一个逻辑计数器,不是物理计数器.
  • 字节码解释器工作时,就是通过改变计数器的值来选取下一条需要执行的字节码指令.
  • 为了保证线程切换后能恢复到正确的执行位置,计数器和线程是一对一的关系,即**“线程私有”**.
  • 如果当前线程执行的是java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址;而如果是Native方法那这个计数器则为Undefined.
  • 不会发生内存泄漏.

java虚拟机栈

  • 可以认为是java方法执行时的内存模型.
  • 每个方法被执行时都会创建一个栈帧(栈帧中包含的数据如图所示),每个方法的执行,对应栈帧的入栈虚拟机到出虚拟机栈(所以栈不需要GC,它会自动滚蛋)的过程,虚拟机栈中包含多个栈帧.
  • 会出现java.lang.StackOverflowError异常以及java.lang.OutOfMemoryError异常.
  • 栈帧所包含的内容如图所示:
    在这里插入图片描述
    局部变量表: 一般都是一个数组,包含方法执行过程中的所有变量(this啊,布尔啊,对象引用啊各种类型的都包括).

操作数栈(在执行字节码指令过程中使用,即它代表着执行代码的操作): 主要包括入栈,出栈,复制,交换,产生消费变量几个步骤.

局部变量表主要是为操作数栈进行数据支持.

举例:
在这里插入图片描述
如图所示: 每个长方形代表一个栈帧(随时都只有一个哈,只是为了表示执行步骤才这么画的),操作数栈的深度为2,局部变量表的深度为3,store表示出操作数栈,load表示入操作数栈.


递归为什么会引发java.lang.StackOverflowError异常

线程执行一个方法时,就会随之创建一个栈帧,同时会将栈帧压入到虚拟机栈中,当方法执行完毕后,就会将栈帧出栈,由此我们可以知道,线程当前执行的方法所对应的栈帧一定是在java虚拟机栈顶.

而递归函数不断去调用自身,每次方法调用会涉及下面几个步骤:
-. 每次方法调用都会生成一个栈帧.
-. 同时它会保存当前方法的栈帧状态,将其置于虚拟机栈中.
-. 栈帧上下文切换的时候,会切换到最新的方法栈帧当中.
由于我们每个虚拟机的虚拟机栈的深度是固定的,每次递归调用会导致栈深度的增加,当我们递归次数太多了之后,压入的栈帧数会超过虚拟机栈深度,就报错了.


虚拟机栈为什么会引发java.lang.OutOfMemoruError异常

当虚拟机栈可以动态扩展时,如果无法申请足够多的内存,就会抛出这个异常.


本地方法栈

  • 本地方法栈与虚拟机栈类似,其主要作用于标注为native的方法.

java内存模型之线程共享部分
元空间与永久代的区别

在JDK8及以后,开始将类的元数据放到本地堆内存中,这一块区域就叫做元空间(但是习惯上我们还是将其当做方法区看待,也就是说还是当做在堆外面),该区域在JDK7及以前都是属于永久代的.
元空间和永久代都是用来存储class的相关信息,包括class对象的方法和字段等.
同时我们要注意:元空间和永久代均是方法区的实现,只是实现有所不同.
方法区只是jvm的一种规范,在jdk7之后,原先位于方法区里的字符串常量池被移到了java堆中,并且在JDK8及其之后,使用元空间替代了永久代.

  • 元空间使用本地内存,而永久代使用的是jvm的内存.
  • 元空间没有字符串常量池(这也就说明,我们还是可以把元空间看成一个独立的部分的).

MetaSpace相对于PermGen的优势

  • 字符串常量池存在于永久代中,容易出现性能问题和内存溢出.
  • 类和方法的信息大小难以确定,给永久代的大小指定带来困难.
  • 永久代会为GC带来不必要的复杂性.
  • 方便HotSpot和其他jvm(没有永久代)和Jrockit的集成.

java堆

  • 它是对象实例的分配区域,是java虚拟机管理的最大的一块,被所有线程共享,在虚拟机启动时创建.

  • java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展实现的,通过-Xmx设置最大值和-Xms设置最小值,如果在堆中没有内存完成实例分配并且堆也无法再扩展时,将会抛出OutOfMemory异常.

  • java对是GC的主要区域,可以分为以下几个部分:
    在这里插入图片描述
    JVM三大性能调优参数:-Xms, -Xmx, -Xss的含义
    在这里插入图片描述

  • -Xss: 规定了每个线程虚拟机栈(即堆栈)的大小,默认情况是256k,此配置将会影响此进程中并发线程数的大小.

  • -Xms: 堆的初始值,即该进程刚创建出来的时候,其专属java堆的大小,一旦对象容量超过了java初始堆的容量,java堆就会自动扩容至-Xmx大小.

  • -Xmx: 堆能达到的最大值,通常情况下,我们都将-Xms-Xmx设置为一样的.

Java内存模型中的内存分配策略
内存分配策略有三种,分别为:

  1. 静态存储: 编译时确定每个数据目标在运行时的存储空间需求,因而在编译时就能给他们分配固定的内存空间,这就要求代码里不能有可变数据结构或者嵌套递归等的出现.
  2. 栈式存储: 该分配可称为动态的存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反,数据区需求在编译期未知,运行时才能知道,但是在进入一个程序模块的时候必须要知道数据区所需大小,以此为其分配内存.
  3. 堆式存储: 编译时或运行时进入程序模块时都无法确定数据区大小,动态分配,比如对象实例和可变长度串.

Java内存模型中堆和栈的联系

创建对象,数组时,栈中可以定义变量(被称为对象和数组的引用变量)用于保存该对象在堆中的首地址.
在这里插入图片描述

Java内存模型中堆和栈的区别

  1. 管理方式: 栈自动释放,堆需要GC.
  2. 空间大小: 栈比堆小.
  3. 碎片数量: 栈产生的碎片远小于堆.
  4. 内存分配方式: 栈支持静态和动态分配,而堆仅支持动态分配.
  5. 效率: 栈的效率比堆高.

元空间,堆,线程独占部分间的联系

例子如图:
在这里插入图片描述
从内存角度看上面的代码
在这里插入图片描述

不同JDK版本之间的intern()的区别–JDK6 VS JDK6+

intern()方法的用法:
在这里插入图片描述
JDK6:调用intern()方法时,如果字符串常量池先前已有该字符串对象值,则返回池中该字符串的地址,否则,将此字符串对象添加到字符串常量池中,并且返回该字符串的地址.

JDK6+:调用intern()方法时,如果字符串常量池先前已经有该字符串对象值,则返回池中该字符串常量池的地址.,如果该字符串对象已经存在于java堆中但还没有将值加载到字符串常量池中,则将堆中此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串,同时在堆中创建String对象,并返回String对象的引用(注意不是值在字符串常量池中的地址哈).

JDK6之前,字符串常量池中仅会添加字符串对象或者堆中字符串对象的副本,虽然也是字符串对象,而JDK6以后,字符串常量池中不仅可以添加字符串对象,还可以添加字符串对象在堆中的引用.


JDK1.8

运行结果:
在这里插入图片描述
运行过程分析:
在这里插入图片描述
对于jdk7及其以上版本而言,由于运行时常量池中可以放引用了,所以两个地址有可能一样

  1. string s = new string(“a”); 它会先在字符串常量池中创建一个a,然后在堆中创建一个对象,同时使得对象引用s指向堆中的对象.
  2. s.intern(); 首先会去字符串常量池中看有没有a这个值,如果有,直接返回其在字符串常量池中的地址,如果没有,他就会将对象的引用放到字符串常量池中.
  3. String s2 = “a”; 此时它会去字符串常量池中去找看有没有a的存在,有就直接返回其在字符串常量池中的地址,没有就会在字符串常量池中创建一个a.
  4. System.out.println(s == s2); 此时我们通过上面的分析可以知道,s指向的是堆中的地址,而s2指向的时常量池中a的地址,所以肯定不相等啦!
  5. String s3 = new String(“a”) + new String(“a”);首先会在堆中创建两个值为a的对象,由于此时a在字符串常量池中有了,所以不会在字符串常量池中创建,然后创建一个aa的对象,但是注意此时不会在常量池中创建ab这个值.然后使s3指向堆中值为ab的对象.
  6. s3.intern(); 它去字符串常量池中去找有没有aa这个值,发现没有哈哈哈^^,然后它就会把值为aa的对象的引用放到字符串常量池中去(也就是其地址).
  7. String s4 = “aa”; 它去字符串常量池中找,发现字符串常量池中有aa这个值,直接返回其地址**(该地址也是堆中值为aa对象的地址)**.
  8. System.out.println(s3 == s4); 两个指向的都是堆中值为aa的对象地址,你说相等不相等嘿嘿嘿.

所以说,当直接创建字符串的时候,会自动在字符串常量池中创建对应的值,但是通过+连接两个字符串对象的时候就不会去字符串常量池创建对应的值


JDK1.6

运行结果:
在这里插入图片描述
运行过程分析:
在这里插入图片描述
JDK6及以前,都是放副本进去,所以说肯定不可能一样.

  1. string s = new string(“a”); 它会先在字符串常量池中创建一个a,然后在堆中创建一个对象,同时使得对象引用s指向堆中的对象.
  2. s.intern(); 首先会去字符串常量池中看有没有a这个值,如果有,直接返回其在字符串常量池中的地址,如果没有,他就会将对象的引用放到字符串常量池中.
  3. String s2 = “a”; 此时它会去字符串常量池中去找看有没有a的存在,有就直接返回其在字符串常量池中的地址,没有就会在字符串常量池中创建一个a.
  4. System.out.println(s == s2); 此时我们通过上面的分析可以知道,s指向的是堆中的地址,而s2指向的时常量池中a的地址,所以肯定不相等啦!
  5. String s3 = new String(“a”) + new String(“a”);首先会在堆中创建两个值为a的对象,由于此时a在字符串常量池中有了,所以不会在字符串常量池中创建,然后创建一个aa的对象,但是注意此时不会在常量池中创建ab这个值.然后使s3指向堆中值为ab的对象.
  6. s3.intern(); 它去字符串常量池中去找有没有aa这个值,发现没有哈哈哈^^,然后它就会把值为aa的对象的副本(注意是副本)放到字符串常量池中去(也就是值为aa的字符串).
  7. String s4 = “aa”; 它去字符串常量池中找,发现字符串常量池中有aa这个值,直接返回其地址**(该地址是字符串常量池中aa字符串的的地址)**.
  8. System.out.println(s3 == s4); s3指向的是堆中值为aa的对象地址,而s4指向的是字符串常量池中aa字符串的地址,当然不相等啦嘿嘿嘿.

一个小小的时间线:

  1. jdk7运行时常量池被放到了堆中,同时intern()方法有了些许改变
  2. jdk8以后用元空间取代了永久代
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值