@MrXiao
2018-04-20T18:55:33.000000Z
字数 5863
阅读 958
Java虚拟机
Java虚拟机是Java程序运行的真实环境,要深入理解Java是如何运行的,理解Java虚拟机是必不可少的。典型的例子:内存溢出。当我们理解java虚拟机之后,我们就会明白为何会出现这个问题,我们又当如何去定位并解决这个问题。
通常我们说的JVM内存分布即指运行时数据区。下图为分布概念图:
OutOfMemoryError异常:当在堆中没有内存完成实例分配,且堆也无法再扩展时
Java堆是垃圾回收器的主要工作区域。由于现在垃圾收集器采用的基本都是分代收集算法,所以堆还可以细分为新生代(New/Young)和老年代(Old/Tenured),再细致一点还有Eden区、From Survivior区、To Survivor区。
新生代:新建的对象都由新生代分配内存。常常又被划分为Eden区和Survivor区,Eden空间不足时会把存活的对象转移到Survivor。新生代的大小可有-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。
老年代:存放经过多次垃圾回收仍然存活的对象。
运行时常量池:
OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈。
这里解释一下局部变量表,局部变量表存储方法相关的局部变量,包括基本数据,对象引用和返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
与虚拟机栈类似,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的。
在HotSpot虚拟机中直接把本地方法栈与虚拟机栈二合一
方法区是JVM的规范,是一个逻辑存储区域。永久代是JVM规范的一种实现,并且永久代是HotSpot特有的。
那么,为什么要做出这些转变呢?有以下几点原因:
关于PermGen和Metaspace,可以看下liuxiaopeng的博客。
Java对象实例存放在堆中;常量存放在方法区的常量池;虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区;以上区域是所有线程共享的。栈是线程私有的,存放该方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。
一个Java程序对应一个JVM,一个方法(线程)对应一个Java栈。
Java对象这里指的是引用类型的对象,这里用Student stu=new Student()为例子访问,Student stu作为引用对象,存在与Java虚拟机栈上,new Student()保存在Java堆中,堆中记录Student类型的信息包括方法,接口,对象类型等地址,这些类型的执行的数据存储在方法区中。
Java堆中分配一块句柄池,虚拟机栈中存放句柄池中的地址,句柄池中包括对象示例数据和对象类型数据的地址。
对象中存贮着对象实例数据和类数据的地址,Java栈的引用指向堆中的对象。
这两种访问方式各有优势。
句柄访问: reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身是不需要修改的。
直接指针访问:最大的好处是速度快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常的频繁,因此这类开销积少成多也是非常可观的执行成本。
就HotSpot虚拟机来说,它采用的是直接指针访问。但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也是十分常见的。
Java运行时数据区的几大板块中,程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,随着线程而灭,因此这几个区域的内存分配和回收都具备确定性,在这3个区域不需要过多的考虑GC问题。而堆和方法区则不一样,我们只有在程序运行期间才能知道会创建哪些对象、占用多少内存,这部分内存的分配和回收都是动态的,垃圾收集器关注的也正是这部分内存。
堆中存放着几乎所有的对象实例,在垃圾收集之前,需要判断哪些对象需要被除了,即哪些对象“存活”着,哪些对象已经“死去”。
给对象中添加一个引用计数器,每当有一个地方引用该对象,计数器值就加1,当引用失效时,引用就减1。任何时刻计数器为0的对象就是不可能再被使用的。
缺陷:很难解决象之间互相循环引用的问题。
主流语言(Java、C#)的主流实现,都是通过可达性算法判断对象是否存活。
基本思路:通过一系列成为"GC Roots"的对象作为起始点,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可达,所以它们就会被判定为可回收对象。
在Java中,可作为GC Roots的对象包括下面几种:
在JDK1.2以前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表着另一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但很狭隘,在这种定义下,对象只存在被引用和没有被引用两种状态。我们希望描述这样一类对象:当空间内存足够时,则保留在内存中;如果内存在GC后还非常紧张,则可以抛弃这些对象。很多系统的缓存都符合这种引用场景。
在JDK1.之后,Java对引用概念进行了扩充,分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),强度依次递减。
即使在可达性分析算法中不可达的对象,也并不是“非死不可”,这时候他们处于缓刑状态,要真正宣布一个对象死亡,至少需要经过两次标记过程:
可达性分析后没有与GC Roots相连的引用链,它会被第一次标记并且进行一次筛选。
筛选条件是是否有必要执行finalize()方法。如果没有覆盖finalize()方法,或者finalize()已经被虚拟机调用过,这两种情况都被视为“没有必要执行”。
如果判定为有必要执行finalize()方法,该对象被放置在一个F-Queue的队列中,并稍后由虚拟机建立的低优先级Finalizer线程去执行他,也就是触发对象的finalize()方法。finalize()是对象逃脱死亡的最后一次机会。
稍后GC将对F-Queue中的对象进行第二次小规模标记
如果对象在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,那么第二次标记时它将被移出“即将回收”的集合;如果第二次标记时还没有逃脱,那它基本上就真的会被回收了。
方法区(或者HotSpot中的永久代)主要回收两部分:废弃常量和无用的类。
废弃常量
如果没有任何一个地方引用常量,在GC时如果有必要的话,该常量会被清理出常量池。常量池中的类(接口)、方法、字段的符号引用也是类似的。
无用的类
卸载类就很麻烦了,必须同时满足3个条件:
满足以上条件也只是可以被回收,是否被回收,HotSpot还有其他方法来控制。
GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区。
简单描述几种算法的思路及演变过程
分为“标记”和“清除”两个阶段:
缺点:
为了解决效率问题,出现了复制算法。将可用内存分为大小相等的两块,每次只是用其中一块。当这一块用完,将还存活的复制到另一块,然后再把使用过的内存空间一次清理掉,循环往复。
分为“标记”和“整理”两个阶段:
当前商业虚拟机的垃圾收集都采用“分代收集算法”。
对于HotSpot虚拟机,堆内存分布如下图所示:
新生代中使用 复制算法,老年代使用 标记——整理算法
默认情况下,eden与survivor的大小比例是8:1,可由 -XX:SurvivorRatio=8 设置。所以可用新生代大小为新生代的90%。
以下是算法的基本思路:
推荐一个这个写的很逗可以看下:http://blog.csdn.net/sd4015700/article/details/50109939