1.
不同版本的JDK版本JVM的内存模型还是不太一样的
2.
JVM内存可以分为两中内存区域:线程私有的内存区域和线程共享的内存区域。
2.1.
线程私有的内存区域:
程序计数器:一块比较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,也就是说明确指示当前程序执行在第几行,比如打断点对程序进行调试
java虚拟机栈:
1.和线程相关,不同线程内,即使运行的是同一个方法,也是出于不同的内存当中。
2.和方法有关,即使是同一个线程,递归调用某个方法,每次调用,都会生成该次方法调用的方法栈帧
3.方法调用所生成的栈帧只有在方法返回后才会销毁,否则一直存在
此区域一共会产生以下两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。
比如递归调用时,每次调用都会进行一次入栈,入栈太多就会抛出异常 - 虚拟机在动态扩展时无法申请到足够的内存,会抛出内存溢出:OOM(OutOfMemoryError)异常
内存溢出VS内存泄漏
2.1内存溢出:
概念:应用系统中存在无法回收的内存或使用的内存过多,最终造成程序运行所需的内存大于系统提供的最大内存
结果:程序无法运行
2.2内存泄漏(Memory Leak)
概念:程序中已经动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统内存的浪费
结果:导致程序运行速度减慢甚至系统奔溃等严重后果
一般出现内存泄漏的情况:长生命周期存活的对象,内部存在不使用对象的引用,导致不使用的垃圾对象无法回收
一般程序出现内存泄漏的例子:在使用长期存活的数据结构和数组时都要考虑是否会造成内存泄漏
代码分析
//test()方法调用的时候:传入参数和内存的关系?
//包括值传递和引用传递
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={1,2,3,4,5};
int index=10;
//当index=10时,该此方法的调用:
//1.生成index=10调用的方法栈帧
//2.传入的参数作为调用方法内局部变量的的定义和赋值,这里就是:
//int[] array={1,2,3,4,5};int index=10;
//3.所调用的方法内的其他局部变量
//以上信息全部保存在方法栈帧中
//方法栈帧存在---->方法进入和方法退出操作
//方法进入:线程内JVM虚拟机栈入栈
//方法退出:出栈
test(array,index);
}
}
内存分析:
说明:对象(包括数组)的赋值是引用,基本数据类型和String赋值的是字面量
代码分析
public class JVMStackLook {
private static class Node{
private Node next;
private String name;
public Node(String name){
this.name=name;
}
}
public static void test(Node node){
node.next=new Node("B");
node=new Node("C");
}
public static void main(String[] args) {
Node node=new Node("A");
test(node);
System.out.println(node.name);//A
System.out.println(node.next.name);//B
}
}
内存分析:
本地方法栈:
本 地方法栈与虚拟机栈的作用完全一样,他俩的区别无非是本地方法栈为虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务。
2.2
线程共享区域:
代码分析:
import org.junit.Assert;
import org.junit.Test;
public class StringInMemory {
// true:都是常量池中的字面量
@Test
public void test1(){// 测试通过
String s1 = "hello";
String s2 = "hello";
Assert.assertTrue(s1 == s2);
}
// true:s3 = "he" + "llo"
// JVM在编译期间会进行优化:s3为字面量拼接的字符串"hello",存在常量池
@Test
public void test2(){// 测试通过
String s1 = "hello";
String s2 = "hel" + "lo";
Assert.assertTrue(s1 == s2);
}
// s2创建了以下对象
// 1."hello":存在字符串常量池,如果常量池已有"hello"就不创建
// 2.new String("hello"):存在堆中
// s1为常量池的"hello"对象,s2为堆中的对象new String("hello")
@Test
public void test3(){// 测试不能通过
String s1 = "hello";
String s2 = new String("hello");
Assert.assertTrue(s1 == s2);
}
// 调用intern方法时,如果池已经包含与equals(Object)方法确定的
// 相当于此String对象的字符串,则返回来自池的字符串。 否则,此
// String对象将添加到池中,并返回对此String对象的引用。
// 由此可见,对于任何两个字符串s和t,s.intern() == t.intern()
// 是true当且仅当s.equals(t)是true。
@Test
public void test4(){// 测试通过
String s1 = "hello";
String s2 = new String("hello");
Assert.assertTrue(s1 == s2.intern());
}
// s4是用s2和s3两个常量池中的对象相加新生成的对象,存在堆中
@Test
public void test5(){// 测试不能通过
String s1 = "hello";
String s2 = "hel";
String s3 = "lo";
//new StringBuilder().append().toString--->
// 堆里生成一个String对象,内部value属性保存hello字面量值
String s4 = s2 + s3;
Assert.assertTrue(s1 == s4);
}
@Test
public void test6(){//测试不通过
String s1 = "hello";
String s2 = new StringBuilder("hel").append("lo").toString();
Assert.assertTrue(s1 == s2);
}
@Test
public void test7(){//测试不通过
String s1 = new String("hello");
String s2 = new StringBuilder("hel").append("lo").toString();
Assert.assertTrue(s1 == s2.intern());
}
@Test
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();
//s1.intern()是创建了一个常量池的引用(变量),指向new String("Hello")
//s2.intern()是返回这个引用变量,所以地址都一样
String s2 = new StringBuilder("hel").append("lo").toString();
// s1的引用地址指向堆里边new String("hello")
// s2.intern();
Assert.assertTrue(s1 == s2.intern());
}
@Test
public void test9(){//和上边代码比较,只少了一个s1.intern(),执行失败
String s1 = new String("hel")+new String("lo");
String s2 = new StringBuilder("hel").append("lo").toString();
Assert.assertTrue(s1 == s2.intern());
}
}
Java堆:
在JVM启动时创建,所有的对象实例以及数组都要在堆上分配。如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM。
方法区/数据元:
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。此区域的内存回收主要是针对常量池的回收以及对类型的卸载。当方法区无法满足内存分配需求时,将抛出OOM异常。
运行时常量池:
1.编译期及运行期间产生的常量被放在运行时常量池中。
2.这里所说的常量包括:基本类型、包装类(包装类不管理浮点型,整形只会管理-128到127)和String。
3.类加载时,会查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
直接内存:
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。也可能导致OutOfMemoryError异常出现。