JVM内存模型

113 阅读5分钟


程序计数器:线程私有。比较小的空间。如果线程正在执行一个Java方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果是Native方法,这个计数器值则为空(Undefined)。

虚拟机栈:线程私有。每个方法在执行时都会在栈中创建一个栈帧,栈帧中保存了局部变量表,动态链接,操作数栈,方法出口等。每个方法被调用至其执行完成的过程,就是一个栈帧由入栈到出栈的过程。

本地方法栈:线程私有。基本功能和虚拟机栈相同,但是其执行的方法为Native方法。

:线程共享。此内存区域的唯一目的就是存放对象实例,也是垃圾收集器管理的主要区域。

方法区:线程共享。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。此区域中有一个特殊的存在,运行时常量池。运行时常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。

堆的详情介绍:

堆可以细分为新生代和老年代;新生代中还可以在分为Eden区,From Survivor区,To Survivor区。

堆大小=新生代+老年代。其比例为1:2;该值可以通过参数 –XX:NewRatio来指定

堆大小的可以通过参数-Xms(最小堆内存) -Xmx(最大堆内存)来设置。

1.新生代

程序新创建的对象都是从新生代分配内存。可以通过-Xmn参数来指定新生代的大小。

Eden:From:To = 8:1:1

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor 区域是空闲着的。新生代实际可用的内存空间为 90% 的新生代空间。

GC流程
  1. 在未开始GC的时候,对象只会存在于Eden区和From区中,To区为空的。
  2. 紧接着进行GC,Eden区中所有存活的对象都会被复制到To区,而在From区中,仍存活的对象会根据他们的年龄值来决定去向。
  3. 年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到To区。经过这次GC后,Eden区和From区已经被清空。
  4. 这个时候,FromTo会交换他们的角色,也就是新的To就是上次GC前的From,新的From就是上次GC前的To。不管怎样,都会保证名为To的Survivor区域是空的。
  5. Minor GC会一直重复这样的过程,直到To区被填满,To区被填满之后,会将所有对象移动到年老代中。

Minor GC 和 Full GC 

在了解GC之前,我们先了解一下如何判断一个对象是否是垃圾以及垃圾回收算法。

如何确定某个对象是否是垃圾?

Java中判断一个对象是否为垃圾,主要有两种算法:引用计数算法与可达性算法。

引用计数算法:概念的我就不做详解了,这种算法有缺陷,那就是无法解决循环引用的问题,所以Java中没有使用这个算法。

可达性算法:Java中采用的算法。该算法的基本思想就是通过一系列的 GC Roots 对象作为起点进行搜索,如果在 GC Roots 和一个对象直接没有可达路径,则称为该对象是不可达的。ps:对象不可达不一定是垃圾对象,只少要经过两次标记才能确定该对象是垃圾对象。

GC Roots对象有那些

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

总结下来无非就是:常量,静态变量,栈中的变量可以称为GC Roots对象。

已经知道了什么是垃圾对象,那么现在就让我们进入垃圾回收算法中。

垃圾回收算法

1.Mark-Sweep(标记-清除)算法

该算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有的垃圾对象,清除阶段就是回收被标记的垃圾对象。该算法存在缺陷就是容易产生内存碎片。



2.Copying(复制)算法

该算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。


3.Mark-Compact(标记-整理)算法

该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。


垃圾回收算法就这3种,但是实际应用的时候,并不是单纯的使用一种算法,而是多种混合使用。像现在的JVM使用的垃圾回收算法,一般情况下 新生代使用复制算法,而老年代使用标记-清除算法或者标记-整理算法。这种混合使用的算法,也叫做分代收集算法。

2.老年代

用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:

  1. 大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
  2. 大的数组对象,且数组中无引用外部对象。

老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。