关于java内存模型我还没怎么整明白,我先写着,大家先看看jvm内存模型的面试题先
java内存模型简介
内存简介: 计算机所有程序都是在内存中运行的.
在程序执行的过程中,需要不断地将内存的逻辑地址和物理地址进行映射,找到相关的指令和数据去执行.
32位处理器: 2^32的可寻址范围,即4G.
64位处理器: 2^64的可寻址范围.
内存中地址空间的划分
- 内核空间
- 用户空间(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内存模型中的内存分配策略
内存分配策略有三种,分别为:
- 静态存储: 编译时确定每个数据目标在运行时的存储空间需求,因而在编译时就能给他们分配固定的内存空间,
这就要求代码里不能有可变数据结构或者嵌套递归等的出现
. - 栈式存储: 该分配可称为动态的存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反,数据区需求在编译期未知,运行时才能知道,但是在进入一个程序模块的时候
必须
要知道数据区所需大小,以此为其分配内存. - 堆式存储: 编译时或运行时进入程序模块时都无法确定数据区大小,动态分配,比如
对象实例和可变长度串
.
Java内存模型中堆和栈的联系
创建对象
,数组
时,栈中可以定义变量(被称为对象和数组的引用变量
)用于保存该对象在堆中的首地址
.
Java内存模型中堆和栈的区别
- 管理方式: 栈自动释放,堆需要GC.
- 空间大小: 栈比堆小.
- 碎片数量: 栈产生的碎片远小于堆.
- 内存分配方式: 栈支持静态和动态分配,而堆仅支持动态分配.
- 效率: 栈的效率比堆高.
元空间,堆,线程独占部分间的联系
例子如图:
从内存角度看上面的代码
不同JDK版本之间的intern()的区别–JDK6 VS JDK6+
intern()方法的用法:
JDK6:
当调用intern()方法时,如果字符串常量池先前已有该字符串对象值
,则返回池中该字符串的地址,否则,将此字符串对象添加到字符串常量池中,并且返回该字符串的地址.
JDK6+:
当调用intern()方法时,如果字符串常量池先前已经有该字符串对象值
,则返回池中该字符串常量池的地址.,如果该字符串对象已经存在于java堆中但还没有将值加载到字符串常量池中,则将堆中此对象的引用添加到字符串常量池
中,并且返回该引用;如果堆中不存在,则在池中创建该字符串,同时在堆中创建String对象,并返回String对象的引用(注意不是值在字符串常量池中的地址哈).
JDK6之前,字符串常量池中仅会添加字符串对象或者堆中字符串对象的副本,虽然也是字符串对象
,而JDK6以后,字符串常量池中不仅可以添加字符串对象,还可以添加字符串对象在堆中的引用.
JDK1.8
运行结果:
运行过程分析:
对于jdk7及其以上版本而言,由于运行时常量池中可以放引用了,所以两个地址有可能一样
- string s = new string(“a”); 它会先在
字符串常量池中创建一个a
,然后在堆中创建一个对象
,同时使得对象引用s指向堆中的对象. - s.intern(); 首先会去字符串常量池中看有没有
a
这个值,如果有,直接返回其在字符串常量池中的地址,如果没有,他就会将对象的引用放到字符串常量池中. - String s2 = “a”; 此时它会去字符串常量池中去找看有没有
a
的存在,有就直接返回其在字符串常量池中的地址,没有就会在字符串常量池中创建一个a
. - System.out.println(s == s2); 此时我们通过上面的分析可以知道,s指向的是堆中的地址,而s2指向的时常量池中a的地址,所以
肯定不相等
啦! - String s3 = new String(“a”) + new String(“a”); 它
首先会在堆中创建两个值为a的对象
,由于此时a在字符串常量池中有了,所以不会在字符串常量池中创建,然后创建一个
aa的对象
,但是注意此时不会在常量池中创建ab
这个值.然后使s3指向堆中值为ab的对象
. - s3.intern(); 它去字符串常量池中去找有没有
aa
这个值,发现没有哈哈哈^^
,然后它就会把值为aa的对象的引用放到字符串常量池中去(也就是其地址)
. - String s4 = “aa”; 它去字符串常量池中找,发现字符串常量池中有
aa
这个值,直接返回其地址**(该地址也是堆中值为aa对象的地址)**. - System.out.println(s3 == s4); 两个指向的都是
堆中值为aa的对象地址
,你说相等不相等嘿嘿嘿.
所以说,当直接创建字符串的时候,会自动在字符串常量池中创建对应的值,但是通过+连接两个字符串对象的时候就不会去字符串常量池创建对应的值
JDK1.6
运行结果:
运行过程分析:
JDK6及以前,都是放副本进去,所以说肯定不可能一样.
- string s = new string(“a”); 它会先在
字符串常量池中创建一个a
,然后在堆中创建一个对象
,同时使得对象引用s指向堆中的对象. - s.intern(); 首先会去字符串常量池中看有没有
a
这个值,如果有,直接返回其在字符串常量池中的地址,如果没有,他就会将对象的引用放到字符串常量池中. - String s2 = “a”; 此时它会去字符串常量池中去找看有没有
a
的存在,有就直接返回其在字符串常量池中的地址,没有就会在字符串常量池中创建一个a
. - System.out.println(s == s2); 此时我们通过上面的分析可以知道,s指向的是堆中的地址,而s2指向的时常量池中a的地址,所以
肯定不相等
啦! - String s3 = new String(“a”) + new String(“a”); 它
首先会在堆中创建两个值为a的对象
,由于此时a在字符串常量池中有了,所以不会在字符串常量池中创建,然后创建一个
aa的对象
,但是注意此时不会在常量池中创建ab
这个值.然后使s3指向堆中值为ab的对象
. - s3.intern(); 它去字符串常量池中去找有没有
aa
这个值,发现没有哈哈哈^^
,然后它就会把值为aa的对象的副本(注意是副本)放到字符串常量池中去(也就是值为
aa的字符串)
. - String s4 = “aa”; 它去字符串常量池中找,发现字符串常量池中有
aa
这个值,直接返回其地址**(该地址是字符串常量池中aa字符串的的地址)**. - System.out.println(s3 == s4); s3指向的是堆中值为
aa
的对象地址,而s4指向的是字符串常量池中aa
字符串的地址,当然不相等啦嘿嘿嘿.
一个小小的时间线:
- jdk7运行时常量池被放到了堆中,同时intern()方法有了些许改变
- jdk8以后用元空间取代了永久代