[关闭]
@MrXiao 2018-04-20T18:55:33.000000Z 字数 5863 阅读 958

Java虚拟机:内存模型

Java虚拟机


Java虚拟机是Java程序运行的真实环境,要深入理解Java是如何运行的,理解Java虚拟机是必不可少的。典型的例子:内存溢出。当我们理解java虚拟机之后,我们就会明白为何会出现这个问题,我们又当如何去定位并解决这个问题。

Java执行过程

1、JVM内存分布

通常我们说的JVM内存分布即指运行时数据区。下图为分布概念图:

运行时数据区

1.1 堆(Heap)

1.2 方法区(Method Area)

运行时常量池

1.3 程序计数器(Program Counter Register)

1.4 虚拟机栈(VM Stack)

1.5 本地方法栈(Native Method Stack)

与虚拟机栈类似,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的。

在HotSpot虚拟机中直接把本地方法栈与虚拟机栈二合一

1.6 直接内存(Direct Memory)

1.7 永久代(PermGen)的发展

方法区是JVM的规范,是一个逻辑存储区域。永久代是JVM规范的一种实现,并且永久代是HotSpot特有的。

那么,为什么要做出这些转变呢?有以下几点原因:

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  4. Oracle 可能会将HotSpot 与 JRockit 合二为一。

关于PermGen和Metaspace,可以看下liuxiaopeng的博客

1.8 小结

Java对象实例存放在堆中;常量存放在方法区的常量池;虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区;以上区域是所有线程共享的。栈是线程私有的,存放该方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。

一个Java程序对应一个JVM,一个方法(线程)对应一个Java栈。

2、对象访问方式

Java对象这里指的是引用类型的对象,这里用Student stu=new Student()为例子访问,Student stu作为引用对象,存在与Java虚拟机栈上,new Student()保存在Java堆中,堆中记录Student类型的信息包括方法,接口,对象类型等地址,这些类型的执行的数据存储在方法区中。

2.1 句柄访问

Java堆中分配一块句柄池,虚拟机栈中存放句柄池中的地址,句柄池中包括对象示例数据和对象类型数据的地址。
句柄访问

2.2 直接指针访问

对象中存贮着对象实例数据和类数据的地址,Java栈的引用指向堆中的对象。

直接指针访问

2.3 小结

这两种访问方式各有优势。

句柄访问: reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身是不需要修改的。

直接指针访问:最大的好处是速度快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常的频繁,因此这类开销积少成多也是非常可观的执行成本。

就HotSpot虚拟机来说,它采用的是直接指针访问。但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也是十分常见的。

3、哪些对象需要回收?

Java运行时数据区的几大板块中,程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,随着线程而灭,因此这几个区域的内存分配和回收都具备确定性,在这3个区域不需要过多的考虑GC问题。而堆和方法区则不一样,我们只有在程序运行期间才能知道会创建哪些对象、占用多少内存,这部分内存的分配和回收都是动态的,垃圾收集器关注的也正是这部分内存。

堆中存放着几乎所有的对象实例,在垃圾收集之前,需要判断哪些对象需要被除了,即哪些对象“存活”着,哪些对象已经“死去”。

3.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用该对象,计数器值就加1,当引用失效时,引用就减1。任何时刻计数器为0的对象就是不可能再被使用的。

缺陷:很难解决象之间互相循环引用的问题。

3.2 可达性算法

主流语言(Java、C#)的主流实现,都是通过可达性算法判断对象是否存活。

基本思路:通过一系列成为"GC Roots"的对象作为起始点,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可达,所以它们就会被判定为可回收对象。
可达性算法

在Java中,可作为GC Roots的对象包括下面几种:

3.3 再谈引用

在JDK1.2以前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表着另一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但很狭隘,在这种定义下,对象只存在被引用和没有被引用两种状态。我们希望描述这样一类对象:当空间内存足够时,则保留在内存中;如果内存在GC后还非常紧张,则可以抛弃这些对象。很多系统的缓存都符合这种引用场景。

在JDK1.之后,Java对引用概念进行了扩充,分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),强度依次递减。

3.4 生存还是死亡

即使在可达性分析算法中不可达的对象,也并不是“非死不可”,这时候他们处于缓刑状态,要真正宣布一个对象死亡,至少需要经过两次标记过程:

  1. 可达性分析后没有与GC Roots相连的引用链,它会被第一次标记并且进行一次筛选。

    筛选条件是是否有必要执行finalize()方法。如果没有覆盖finalize()方法,或者finalize()已经被虚拟机调用过,这两种情况都被视为“没有必要执行”。
    如果判定为有必要执行finalize()方法,该对象被放置在一个F-Queue的队列中,并稍后由虚拟机建立的低优先级Finalizer线程去执行他,也就是触发对象的finalize()方法。finalize()是对象逃脱死亡的最后一次机会。

  2. 稍后GC将对F-Queue中的对象进行第二次小规模标记

    如果对象在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,那么第二次标记时它将被移出“即将回收”的集合;如果第二次标记时还没有逃脱,那它基本上就真的会被回收了。

3.5 回收方法区

方法区(或者HotSpot中的永久代)主要回收两部分:废弃常量和无用的类。

  1. 废弃常量

    如果没有任何一个地方引用常量,在GC时如果有必要的话,该常量会被清理出常量池。常量池中的类(接口)、方法、字段的符号引用也是类似的。

  2. 无用的类

    卸载类就很麻烦了,必须同时满足3个条件:

    • 该类所有实例都已被回收
    • 加载该类的ClassLoader已经被会后
    • 该类对应的Class对象没有在任何地方呗引用,无法通过反射访问该类的方法

    满足以上条件也只是可以被回收,是否被回收,HotSpot还有其他方法来控制。

4、何时回收?

GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区。

  1. 对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,Minor GC非常频繁,而且速度也很快;
  2. Full GC,发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。
  3. JDK 6 Update 24之后,HandlePromotionFailure无效。只要老年代剩余连续空间大于新生代对象总和或者历次晋升的平均大小就会进行Minor GC,否则Full GC。
    发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则进行一次Full GC,如果小于,则查看是否允许担保失败(HandlePromotionFailure=true/false),如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。

5、如何回收?

简单描述几种算法的思路及演变过程

5.1 标记——清除算法

分为“标记”和“清除”两个阶段:

缺点:

标记——清除算法

5.2 复制算法

为了解决效率问题,出现了复制算法。将可用内存分为大小相等的两块,每次只是用其中一块。当这一块用完,将还存活的复制到另一块,然后再把使用过的内存空间一次清理掉,循环往复。

复制算法

5.3 标记——整理算法

分为“标记”和“整理”两个阶段:

此处输入图片的描述

5.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集算法”。

对于HotSpot虚拟机,堆内存分布如下图所示:

分代收集算法下的堆内存分布

新生代中使用 复制算法,老年代使用 标记——整理算法

默认情况下,eden与survivor的大小比例是8:1,可由 -XX:SurvivorRatio=8 设置。所以可用新生代大小为新生代的90%。

以下是算法的基本思路:

  1. 对象新建时,首先会在Eden区创建,年龄为0,直到GC时,若没有消亡,则放入servivor区,年龄为1
  2. 进入survivor区也不是安全的,当下一次Minor GC来的时候,会检查Eden区和使用的Survivor区,若有对象存活,则放入另一个Survivor区,年龄加1
  3. 当两个Survivor区切换几次后,对象年龄增长到15(默认,-XX:MaxTenuringThreshold=15),则进入老年代。
  4. 进入老年代也不是安全的,当老年代空间不足时,触发Major GC,已经消亡的对象还是会被干掉。

推荐一个这个写的很逗可以看下:http://blog.csdn.net/sd4015700/article/details/50109939

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注