Java 内存模型

第一部分 内存模型

我们常说Java内存模型(Java Memory Model, JMM)指的是Java运行时(Java runtime)内存模型,Java源码通过javac 翻译成字节码,由即时编译器(JIT)编译执行。因为字节码是静态代码,需要加载到内存才能成为可以动态运行的对象。运行时内存数据区大体上被分为5个区域、两大类型, 如下图。

内存模型

线程私有(隔离)数据区

1、程序计数器,记录正在执行的虚拟机字节码的地址;
2、虚拟机栈:方法执行时创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法一个栈帧,互不干扰。
3、本地方法栈:虚拟机的Native方法执行的内存区;

线程共享数据区:

1、Java堆:存放对象的区域;
2、方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据;
3、常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。

第二部分 模型详情

运行时内存分为五大块区域,Java内存总体结构图如下:

这里写图片描述

2.1 程序计数器

当前线程所执行的字节码行号指示器。每个线程都有自己计数器,是私有内存空间,各个线程之间计数器互不影响,独立存储。
当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)。

2.2 虚拟机栈

虚拟机栈,生命周期与线程相同,是Java方法执行的内存模型。每个方法(不包含native方法)执行的同时都会创建一个栈帧结构,方法执行过程,对应着虚拟机栈的入栈到出栈的过程。

2.2.1 栈帧(Stack Frame)结构

栈帧是用于支持虚拟机进行方法执行的数据结构,是属性运行时数据区的虚拟机站的栈元素。

2.2.2 栈帧

局部变量表
一组变量存储空间, 容量以slot为最小单位。操作栈(stack大小,编译期确定),操作栈元素的数据类型必须与字节码指令序列严格匹配
动态连接
指向运行时常量池中该栈帧所属方法的引用,为了 动态连接使用。前面的解析过程其实是静态解析;对于运行期转化为直接引用,称为动态解析。
方法返回地址
正常退出,执行引擎遇到方法返回的字节码,将返回值传递给调用者
异常退出,遇到Exception,并且方法未捕捉异常,那么不会有任何返回值。
额外附加信息
虚拟机规范没有明确规定,由具体虚拟机实现。

Java虚拟机规范规定该区域有两种异常:

StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出
OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出

2.3 本地方法栈

本地方法栈则为虚拟机使用到的Native方法提供内存空间,而前面讲的虚拟机栈式为Java方法提供内存空间。

异常(Exception):Java虚拟机规范规定该区域可抛出StackOverFlowError和OutOfMemoryError。

2.4 Java堆

Java堆,是Java虚拟机管理的最大的一块内存,里面存放的是几乎所有的对象实例和数组数据。

从内存回收角度,Java堆被分为新生代和老年代;这样划分的好处是为了更快的回收内存;
从内存分配角度,Java堆可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);这样划分的好处是为了更快的分配内存;

对象创建的过程是在堆上分配着实例对象,那么对象实例的具体结构如下:

这里写图片描述

对于填充数据不是一定存在的,仅仅是为了字节对齐。HotSpot VM的自动内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例数据不是8的倍数,便需要填充数据来保证8字节的对齐。该功能类似于高速缓存行的对齐。

堆的结构

这里写图片描述

首先堆可以划分为新生代和老年代,
然后新生代又可以划分为一个Eden区和两个Survivor(幸存)区。

按照规定,新对象会首先分配在Eden中(如果对象过大,比如大数组,将会直接放到老年代)。在GC中,Eden中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过minor GC的次数),会被移动到老年代。新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

垃圾回收

垃圾回收的意义
分代收集。即在新生代和老生代使用不同的收集方式。在垃圾收集上,目标主要有:加大系统吞吐量(减少总垃圾收集的资源消耗);
减少最大STW(Stop-The-World)时间;减少总STW时间。不同的系统需要不同的达成目标。而分代这一里程碑式的进步首先极大减少了STW,然后可以自由组合来达到预定目标。

可达性检测
引用计数:一种在jdk1.2之前被使用的垃圾收集算法,我们需要了解其思想。其主要思想就是维护一个counter,当counter为0的时候认为对象没有引用,可以被回收。缺点是无法处理循环引用。
根搜算法:思想是从gc root根据引用关系来遍历整个堆并作标记,称之为mark,
之后回收掉未被mark的对象,好处是解决了循环依赖这种,
这里的gc root主要指:

a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象

整理策略

复制:主要用在新生代的回收上,通过from区和to区的来回拷贝。需要特定的结构(也就是Young区现在的结构)来支持,对于新生成的对象来说,频繁的去复制可以最快的找到那些不用的对象并回收掉空间。所以说在JVM里YGC一定承担了最大量的垃圾清除任务。

标记清除/标记整理:主要用在老生代回收上,通过根搜的标记然后清除或者整理掉不需要的对象。

2.5 方法区

方法区主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。GC在该区域出现的比较少。异常(Exception):Java虚拟机规范规定该区域可抛出OutOfMemoryError。

2.6 运行时常量池

运行时常量池也是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。运行时常量池除了编译期产生的Class文件的常量池,还可以在运行期间,将新的常量加入常量池,

第三部分 Java中的内存泄露问题

 虽然Java拥有垃圾回收机制,但同样会出现内存泄露问题,比如下面提到的几种情况:

(1)、 例如 HashMap、Vector 等集合类的静态使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。

private static Vector vector = new Vector();
  public void TestGc() {
      for (int i = 0; i < 1000; i++) {
          Object object = new Object();
          vector.add(object);
          object = null;
      }
  }

上面的代码中,虚拟机栈中保存者 Vector 对象的引用 vector 和 Object 对象的引用 object 。在 for 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 object 引用置空。问题是虽然我们将 object 引用置空,但当发生垃圾回收时,我们创建的 Object 对象也不能够被回收。因为垃圾回收在跟踪代码栈中的引用时会发现 vector 引用,而继续往下跟踪就会发现vector引用指向的内存空间中又存在指向 Object 对象的引用。也就是说,尽管object引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。
  
(2)、非静态内部类持有外部类的应用,容易导致内存泄漏

(3)、 各种资源连接包括数据库连接、网络连接、IO连接等没有显式调用close关闭,不被GC回收导致内存泄露。

(4)、监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值