一、线程私有的内存区域
1.Java虚拟机栈
每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。
(1)和线程相关:不同线程内,即使运行同一个方法,也是处于不同内存
(2)和方法有关:即使是同一个线程,递归调用某个方法,每次调用都会生成该次方法调用的方法栈帧。
例1:
public class JVMStackLook {
public static void test(int[] array,int index){
System.out.println(index);
if (index <= 2){
return;
}
test(array,index-1);
}
public static void main(String[] args) {
int[] array = {4,5,10,1,15};
test(array,10);
}
}
流程图:
局部变量定义赋值时,对象(包括数组)赋值的是引用,基本数据类型和String赋值的是字面量。
例2:
public class JVMStackLook {
public static void main(String[] args) {
Node node = new Node("A");
test(node);
System.out.println(node.name);
System.out.println(node.next.name);
}
public static void test(Node node){
node.next = new Node("B");
node = new Node("C");
}
public static class Node{
private Node next;
private String name;
public Node(String name){
this.name = name;
}
}
}
流程图:
Java虚拟机栈区域一共会产生以下两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。
StackOverFlowError异常:方法调用太深,比如递归,每次调用都是一次入栈,入栈的数量太多,就抛出这个异常。 - 虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutOfMemoryError)异常。
OOM(OutOfMemoryError)异常内存溢出:线程调用方法,创建方法该次调用的栈帧,内存不足抛出OOM异常。
面试题:内存溢出和内存泄漏的区别?
内存泄漏Memory Leak:程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
- 一般出现内存泄漏的情况:长生命周期存活的对象,内部持有不使用对象的引用,导致不使用的垃圾对象无法回收。
- 一般程序经常出现内存泄漏的例子:在使用长期存活的数据结构、数组时,都要考虑对象引用导致内存泄漏的问题。
内存溢出(Out Of Memory,简称OOM):应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。
结果:此时程序就运行不,系统会提示内存溢出(进程都会挂掉,情况严重)
2.本地方法栈
本地方法栈与虚拟机栈的作用完全一样,他俩的区别无非是本地方法栈为虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务。
二、线程共享区域
1.Java堆
在JVM启动时创建,所有的对象实例以及数组都要在堆上分配。
如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM。
2.方法区/元数据区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
此区域的内存回收主要是针对常量池的回收以及对类型的卸载。当方法区无法满足内存分配需求时,将抛出OOM异常。
3.运行时常量池
编译期及运行期间产生的常量被放在运行时常量池中。
这里所说的常量包括:基本类型、包装类(包装类不管理浮点型,整形只会管理-128到127)和String。
类加载时,会查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
4.直接内存
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。也可能导致OutOfMemoryError异常出现。
5.Class文件常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
编译时生成的信息,要在运行后,类加载时把变量和对象的引用在内存中关联起来。编译时无法关联,所以使用符号引用间接关联。
6.运行时常量池
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
7.字符串常量池
存储字符串对象,或是字符串对象的引用。
例一:
public class StringInMemory {
public void test1(){// 测试通过
String s1 = "hello";
String s2 = "hello";
Assert.assertTrue(s1 == s2);
}
}
true:因为都是常量池中的字面量。
例二:
public class StringInMemory {
public void test2(){// 测试通过
String s1 = "hello";
String s2 = "hel" + "lo";
Assert.assertTrue(s1 == s2);
}
}
true:s2 = “hel” + “lo”
JVM在编译期间会进行优化:s2为字面量拼接的字符串"hello",存在常量池
例三:
public class StringInMemory {
public void test3(){// 测试不能通过
String s1 = "hello";
String s2 = new String("hello");
Assert.assertTrue(s1 == s2);
}
}
s2创建了以下对象
1.“hello”:存在字符串常量池,如果常量池已有"hello"就不创建
2.new String(“hello”):存在堆中。
s1为常量池的"hello"对象,s2为堆中的对象new String(“hello”)
例四:
public class StringInMemory {
public void test4(){// 测试通过
String s1 = "hello";
String s2 = new String("hello");
Assert.assertTrue(s1 == s2.intern());
}
}
调用intern方法时,如果池已经包含与equals(Object)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。由此可见,对于任何两个字符串s和t,s.intern() == t.intern() 是true当且仅当s.equals(t)是true。
例五:
public class StringInMemory {
public void test5(){// 测试不能通过
String s1 = "hello";
String s2 = "hel";
String s3 = "lo";
String s4 = s2 + s3;
Assert.assertTrue(s1 == s4);
}
}
// s4是用s2和s3两个常量池中的对象相加新生成的对象,存在堆中
例六:
public class StringInMemory {
public void test8(){//执行成功
// 方法栈:s1局部变量
// 常量池生成的对象:hel,lo。注意常量池没有生成hello对象
// 堆生成的对象:new String("hel")、new String("lo")、字符串+操作生成的new String("hello")
String s1 = new String("hel")+new String("lo");
// 字符串常量池中,获取或创建一个字符串对象或引用
// 字符串常量池创建一个引用,指向s1指向的对象new String("hello")
s1.intern();
String s2 = new StringBuilder("hel").append("lo").toString();
// s1的引用地址指向堆里边new String("hello")
// s2.intern();
Assert.assertTrue(s1 == s2.intern());
}
}
false
例七:
public class StringInMemory {
public void test7(){
String s1 = new String("hello");
String s2 = new StringBuilder("hel").append("lo").toString();
Assert.assertTrue(s1 == s2.intern());
}
}
true
例八:
public class StringInMemory {
public void test8(){
String s1 = "hello";
String s2 = new StringBuilder("hel").append("lo").toString();
Assert.assertTrue(s1 == s2);
}
}
false