Java虚拟机之内存

1 概述

Java内存包含以下几部分:
方法区/虚拟机栈/本地方法栈/堆/程序计数器

  1. 程序计数器:每个线程都有一个独立的程序计数器,即程序计数器是线程私有的。用以实现多线程切换时恢复状态。其生命周期与线程相同。

  2. 虚拟机栈:Java虚拟机栈也是线程私有的,其生命周期与线程相同,其描述的是Java方法执行模型,由-Xss设置。内存不足可能造成StackOverflowError/OutOfMemeryError异常。

  3. 本地方法栈:是为Native方法服务的。类似Java虚拟机栈为Java方法服务。也是线程私有的,其生命周期与线程相同。

  4. 堆:Java堆是所有线程共享的内存区域。用于存放对象实例,Java堆是垃圾回收机制管理的主要区域。大小为-Xmx和-Xms控制的

  5. 方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。大小为-XX:MaxPermSize设置的上限。

  6. 运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。加载类的时候加载这些数据。

  7. 直接内存NIO,可以使用Native函数库直接申请堆外内存,用于在Java堆和Native堆中共享数据,避免复制,提高效率。

2 对象

2.1 对象的创建

虚拟机遇到一条new指令时

  1. 首先将去检查这个指令的参数是否能再常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。若没有则先加载。(具体分析参考Java虚拟机之类加载

  2. 类检查通过后,虚拟机将对新生对象分配内存,即从堆中划分一块指定大小等于对象大小的内存,由于分配内存是高并发操作,还需要考虑并发情况处理;且内存操作涉及到GC,可以参考下面GC部分内容。

  3. 内存分配完成后,虚拟机需要将分配到的内存空间初始化为零值,这里的初始化不包括对象头。如此已到达,默认的Java对象的实例字段初始值都是零值。

  4. 虚拟机对对象进行必要的设置,这里主要是处理对象头中的信息,包括类的元数据信息,哈希码,GC分代年龄等。

  5. 此时,对于虚拟机而言,对象创建完成了,接下来要做的操作是,执行Java层的方法,把对象按照Java中设定的意愿进行初始化。此时一个对象才算创建完成。

2.2的内存布局

在HotSpot虚拟机,对象在内存中的存储布局分为3部分:

对象头(Head)/实例数据(Instance Data)/对齐填充(Padding)
  1. 对象头包含两部分信息:

    一是存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等;

    另一部分是类型指针,即对象指向的类元数据的指针,虚拟机通过这个指针确定这个对象时哪个类的实例。若对象是一个数组,则对象头中还必须有一块用于记录数组长度的数据。

  2. 实例数据:对象真正存储的有效信息,程序代码中定义的各种类型的字段内容,包括从父类继承的以及当前类中定义的。HotSpot虚拟机的存储策略是:longs/doubles;ints;shorts/chars;bytes;booleans;oops(Ordinary Object Pointer),父类的变量会出现在当前类前面。另外,HotSpot要求对象起始地址必须是8字节对齐的,故头部可能需要通过对齐填充来补全。

2.3 对象的访问定位

一般有两种方式访问对象

  1. 使用句柄,Java堆中将会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址。而句柄中包含了对象实例数据域类型数据各自的具体地址信息。这种方式的好处是,GC或其他操作导致对象移动的时候只需要改变句柄值即可。
  2. 第二种方式是直接使用指针访问,HotSpot就是使用这种方式。它的好处是节省了一次指针定位的开销。

2.4 OOM

在Java虚拟机的规范描述中,除了程序计数器,其他几个运行时区域都有发生OOM的可能。

  1. Java堆溢出:申请内存大于堆最大值且无法被回收,则可能造成堆溢出。

    -Xms 用来设置Java堆大小的最小值;-Xmx 用来设置Java堆大小的最大值;

    可以通过MAT工具获取异常的DUMP文件,然后分析泄露的对象到GC Root的引用链,以判断该对象时为何无法被回收的。

  2. 虚拟机和Native方法栈溢出

    HotSpot并不区分本地方法栈和虚拟机栈,故放在一起讨论。

    -Xss 设置Java栈
    -Xoss 设置本地方法栈(HotSpot无效)

    若线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError;
    若虚拟机在扩展栈时无法申请到足够的内存空间,则会抛出OutOfMemoryError;

  3. 方法区和运行时常量池溢出

    通过-XX:PermSize和-XX:PermMaxSize限制方法区的大小。

    String.intern()是一个Native方法,作用是若常量池中已经包含一个等于此对象的字符串,则返回池中这个字符串的String对象;否则将此字符串添加到常量池中,并且返回此String对象的引用。

  4. 直接内存溢出

    -XX:MaxDirectMemorySize指定直接内存最大容量,不指定时默认同Java堆大小。

3 GC

3.1 引用计数

给对象添加一个引用计数器,每引用一次,计数器加1;每当引用失效时,计数器减1.

3.2 可达性分析

GCRoots到对象的路径不存在则回收。可作为GCRoot的对象一般为:虚拟机栈中(栈帧中的本地变量表)中引用的对象;方法区中的静态属性引用的对象;方法区常量引用的对象;本地方法栈中的JNI引用的对象。

3.3 引用分类

  1. 强引用 StrongReference

    new出来的对象一般都是强引用。垃圾回收期不会回收此类引用。

  2. 软引用 SoftReference

    描述一些还有用但并非必需的对象。内存不够的时候,GC才会回收软引用

  3. 弱引用 WeakReference

    用来描述非必须对象的,比软引用更弱。GC的时候会直接回收弱引用

  4. 虚引用 PhantomReference

    虚引用的存在不影响GC对内存的回收。

4 垃圾收集算法

4.1 标记-清除算法

首先标记需要回收的对象,然后再回收这些对象。此算法是垃圾回收思想的基础。它存在的缺陷是效率不高,以及会造成内存不连续。

4.2 复制算法

将内存区域分成大小相等的两块,回收的时候,将存活的对象从一块拷贝到另一块,并清空原内存块。主要是内存消耗太大。

目前HotSpot的新生代区域使用的是此算法的优化版本,将新生代内存分为一块Eden和两块Survivor。Eden和Survivor的比例是8:1。回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor中。这里涉及到内存不够是,老年代担保的问题后续会分析。

4.3 标记-整理算法

先标记出要回收的对象,然后其他对象统一向一端移动,覆盖掉需要回收的内存区域,最后直接清理掉端边界后面的内存区域,已达到目的。

4.4 分代收集算法

将内存分为不同区域,如HotSpot的新生代和老年代。不同区域使用不同的回收算法。

5 内存分配与回收策略

  1. 对象优先在新生代Eden上分配。当Eden上空间不足时,虚拟机将触发Minor GC。
  2. 大对象直接进入老年代。
  3. 长期存活的对象将进入老年代

6 查看GC日志

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值