
关于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内存模型中的内存分配策略
内存分配策略有三种,分别为:
1. 静态存储: 编译时确定每个数据目标在运行时的存储空间需求,因而在编译时就能给他们分配固定的内存空间,这就要求代码里不能有可变数据结构或者嵌套递归等的出现.
2. 栈式存储: 该分配可称为动态的存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反,数据区需求在编译期未知,运行时才能知道,但是在进入一个程序模块的时候必须要知道数据区所需大小,以此为其分配内存.
3. 堆式存储: 编译时或运行时进入程序模块时都无法确定数据区大小,动态分配,比如对象实例和可变长度串.
Java内存模型中堆和栈的联系
创建对象,数组时,栈中可以定义变量(被称为对象和数组的引用变量)用于保存该对象在堆中的首地址.
Java内存模型中堆和栈的区别
- 管理方式: 栈自动释放,堆需要GC.
- 空间大小: 栈比堆小.
- 碎片数量: 栈产生的碎片远小于堆.
- 内存分配方式: 栈支持静态和动态分配,而堆仅支持动态分配.
- 效率: 栈的效率比堆高.
元空间,堆,线程独占部分间的联系
例子如图:
从内存角度看上面的代码
不同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及以前,都是放副本进去,所以说肯定不可能一样.
- 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字符串的地址,当然不相等啦嘿嘿嘿.
- s.intern(); 首先会去字符串常量池中看有没有
一个小小的时间线:
- jdk7运行时常量池被放到了堆中,同时intern()方法有了些许改变
- jdk8以后用元空间取代了永久代




近期评论