虚拟机之深入浅出

来源:互联网 发布:淘宝内衣模特雪碧 编辑:程序博客网 时间:2024/06/06 14:02

java最有特色非它的虚拟机莫属。本篇博客是我的读书笔记,总结我对java虚拟机的了解。这篇博客先介绍基础知识,比较理论,但认真读完会对虚拟机有进一步的了解。

一.相关概念

在了解虚拟机之前我们要明确以下几个基本概念:

jdk :java程序设计语言,java虚拟机,javaAPI类库统称为JDK。
jre:java API类库中的java SE API子集和java虚拟机统称为JRE。
java ME:支持java程序运行在移动端上。
javaSE:支持面向桌面级的java平台。它提供了完整的API。
javaEE:支持使用多层架构的企业应用,做了大量的扩充。

二,java虚拟机的内存分析

java虚拟机分为以下几部分:方法区,虚拟机栈,本地方法栈,堆,程序计数器。以下分别介绍下各自的内容:

1.程序计数器
是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。由于java多线程是线程轮流切换并分配处理器执行时间的,所以为了切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。各线程计数器之间互不影响,这样的内存叫“线程私有”的内存。
如果线程执行的是一个方法,它记录的是正在执行的虚拟机字节码指令的地址。如果是native方法,这个计数器为空。它是唯一一个在java中没规定八条outMemoryError的区域。
2.java虚拟机栈
java虚拟机栈它也是线程私有的,它的生命周期与线程相同。描述的是方法的内存模型,每个方法在执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程。一般平时常说的栈就指它,或者是它中的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型(byte,char等)、对象引用(reference类型,它不等同于对象,可能是一个对象起始地址的引用 指针。也可能是一个代表对象的句柄)和returnAddress类型。
其中64位长度的long和double类型会占用2个局部变量空间(slot),其余的数据类型只占用一个。局部变量所需的内存在编译期完成分配,在方法的运行期间不会改变 。在java虚拟机中它有两种异常:1.线程请求栈深度大于虚拟机允许的深度,抛出stackOverFlowerError,如果扩展无法申请到足够内存,会抛出outOfMemoryError。
3.本地方法栈
它提供虚拟机使用到手Native方法服务。而上面的提到的和虚拟机栈执行java方法(字节码)。
4.java堆
java堆是虚拟机所管理的内存最大的一块。它是被所有线程共享的一块内存区域。唯一的目的就是存放对象实例。几乎所有的对象实例都在这里分配内存。它是垃圾收集器管理的主要区域,因此也被称为“GC堆”。由于现在收集器基本都采用分代收集算法,所以java堆也可以细分为:新生代和老年代。再细致点就是Eden空间、From Survivor空间,To Survivor空间等。线程共享的java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer )。主流虚拟机都是按照可扩展来实现(-Xmx,-Xms)内存分配。
5.方法区
方法区与java堆一样是各个线程共享的内存区域,它用于存储己被虚拟机加载的类信息、常量、静态变量、即时编绎编绎后的代码等。垃圾回收在这个区域较少出现,这个区域内存回收主要针对常量池的回收和对类型的卸载。它不内存不够时也会抛出outOfMemoryError。
6,运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法,接口等信息个,还有一项信息是常量池,它存编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时也可以将新的常量放入池中,如通过string 类的intern方法。
7.直接内存
它并不是虚拟机运行时数据区的一部分。NIO引入了一个通道和缓冲区的IO方式,它可以使用Native函数库存直接分配堆外内存,然后通过一个存储在java堆上的DirectByteBuffer对象来为这块内存引用进行操作,避免了在java堆和Native来回复制数据。

三虚拟机中对象的介绍

1.对象的创建
当遇到一条new指令时,首先去检查引用代表类是否己被加载,解析和初始化过。如没有就执行相应类加载。给类分配内存有两种方式,一种叫指针碰撞,它是指在内存绝对规整时,只需指针移动相应的位置(Serial,ParNew用的就是它)。另一种叫空闲列表(Free List)它需要维护一个列表,记录哪 些内存是可用的,分配的时候从列表中找到一块足够大的空间划分给对象实例。(CMS)。
分配内存需要同步,有两种方案,一种是对分配内存的运行进行同步处理,保证其原子性,一种是把内存分配的动作按照划分在不同空间中运行,即每个java堆预先分配一小块内存,称酝线程分配缓冲,需要内存就在它上面分配,用完了再分配新的TLAB,这时才需同步。可用-XX:+/-UseTLAB来设定。
内存分配完就给内存空间设置零值。这样java代码中实例可以不赋初始值也获得默认值。
2.对象的内存布局
分为三个区域:对象头,实例数据,对齐填充。
对象头:它包括两部分信息,第一部分是用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持股的锁,偏向线程ID,偏向时间等。 在32位虚拟机中是32bit,在64位是64bit。第二部分是类型指针,即对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象是哪个类实例。如果对象是一个数组,那对象头还要有一块用于记录数组长度的数据 。
实例数据部分:是对象的真正存储有效信息。也就是代码所定义的各种字段内容。无论是从父类继承下来,还是在子类中定义,都需要记录起来。这部分存储顺序会受到虚拟机分配策略参数和字段在java中定义顺序的影响 。父类定义的变量会出现在子类之前。
第三是对齐填充:仅仅起到占位符的作用。
3.对象的访问定位 、
为使用对象 ,需要通过栈上的reference数据来操作堆上的具体对象 。reference在虚拟机中只规定一个指向对象 的引用 。目前主流的有两种方式 :使用句柄和直接指针。
1.使用句柄:堆中会划分出一埠内存来作为句柄池,reference存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息(实例数据在堆中,类型数据在方法区中)。好处是对象被移动时只需改变句柄中的实例数据指针。
2.如果使用指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference存储的直接就是对象的地址。好处是速度快,少了一次指针定位的时间开销。


四,java中引用类型介绍

有4种引用类型:
强引用:指在程序代码中普遍存在的引用。只要强引用还存在,垃圾收集永远不会回收掉被引用的对象。、
软引用:将要发生内存溢出时,会把这些对象列进回收范围之中进行第二次回收。java提供了SoftReference来实现软引用。
弱引用:描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收它。java提供WeakReference来实现弱引用。
虚引用:一个对象是否有虚引用的存在完全不会对其生存时间构成影响。也无法通过虚引用来获得一个对象实例。设置虚拟引用关联唯一目的是能在这个对象被收集器回收时收到一个系统通知。提供PhantomReference来实现虚引用。

五,java判断对象是否存活

程序计数器,虚拟机栈,本地方法栈会随线程而生,也随它而灭。它们是编绎时己知,而堆与方法区一般是运行时才能知道需要多少内存。这部分内存的分配回收也是动态的。如何判断对象是否存在,有两种方法。
1.引用计数算法
给对象添加一个引用计数器,每当有一个地方引用了它,计数加1,当引用失效,计数器减一。当为0 表示对象不会再用。但它不解决对象之间相互循环引用 的问题。
2.可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些点开始向下搜索,走过的路径称为引用链,当没有任何引用链相连时则证明此 对象是不可用的。
可作为GC的对象:
1.虚拟机栈中的引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中JNI引用的对象。

六 垃圾回收的算法分类

这是本博客的重点,也是虚拟机知识体系中的重点。回收算法有以下几种:
1.标记清除:
首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。有两个不足,
* 效率不高,两个过程的效率都不高。
* 空间问题:会出现大量的不连续内存碎片。碎片太多会导致以后在程序运行过程中需要分配较大对象时无法找到足够连续内存而不得不提前触发垃圾收集。
2.复制算法
把内存容量划分为大小相等的两块,每次使用其中一块,内存用完时将还活的复制到另一半上去,再把己使用过的内存清理掉。这样只要移动指针来分配内存。比较高效。代价是内存空间缩小了一半。
商业虚拟机都采用这种收集算法来回收新生代,由一块较大的内存Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。回收时将活着的复制到另一块survivor上,最后清理Eden和刚用过的Survivor,这个比例大概为8:1;当回收时存活的内存大于Survivor的内存,就要依赖老年代来分配担保,存活的对象直接进入老年代。
3.标志整理算法
根据老年代的特点,有人提出一种“标记-整理”算法。标记过程不变。但后续的整理是将存活的对象都向一端移动。然后清理掉边界以外的内存。
4.分代收集算法:
最常见的算法,它并不是新的思路,只是把对象存活周期分为几块,一般分为新生代和老年代。新生代因为每次都有大批对象死去,所以使用复制算法。老年代中因为对象存活率高,没有额外的空间对它进行分配担保,所以要使用标记清除或标记整理算法。

七 HotSpot虚拟机的算法实现

HotSpot可以说是最常用的虚拟机,以下介绍它如何进行GC。在GC的过程中必须停顿所有的java执行线程。这就好比你要打扫房间,不能边打扫边扔垃圾。要停止扔再扫。目前主流的虚拟机是使用准确式的GC,并不需要一个不漏地检查完所有执行上下文和全局引用,虚拟机用一组叫OopMap数据结构在类加载完成时把对象内的偏移量上是什么类型的数据计算出来,在JIT编译过程也会在特定位置记录下栈和寄存器中哪些位置是引用 。然后GC时扫描这些点就可以得知这些信息了。
安全点:因为不可能每条指令都生成一个OopMap,这样会需要大量的额外空间,虚拟机只会在特定的点记录这些信息,而这些点就叫安全点。程序执行时并不是所有地方都能停顿下来,只有到达安全点才会暂停,所以安全点太少会使用GC等待时间变长,安全点太多会使负荷增大。
GC时要使线程停在安全点有两种方式:
1.抢先式:不需要线程执行代码去配合,GC时首先把所有线程全部中断,如果发现有线程不在安全点上就恢复线程,让它跑到安全点上。
主动式(目前主要使用):仅仅是简单设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起轮询标志的地方和安全点是重合的。
安全区域:对于处于sleep或blocked状态的,无法响应JVM走到安全点上,这时就需要安全区域。它是指在一段代码片段中引用关系不会发生变化,这这个区域任何地方开始GC都是安全的。首先要标识自己是安全区域,这时GC时就不用管这些区域,但线程要离开安全区域时,要检查是否己经完成了根节点枚举(整个个GC过程),如果是,继续执行,如果不是等待GC完成后再执行。

最后说说我对内存分配策略的一个了解,总结有以下几点:
1.优先放入在Eden分配。
2.大对象直接进入老年代。
3.长期存活的对象将进入老年代。它有一个Age计数,每熬过一次Minor GC,长一岁。达到一定岁数(默认15)会进入老年代,可通过-XX:MaxTenuringThreshold设置。
4.动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于或等于该年龄的可以直接进入老年代。



0 0
原创粉丝点击