参考资料:《深入理解JAVA虚拟机》
JVM 简介:A为java程序,jre包含了JVM,建立在操作系统之上,应用程序之下
运行时数据区域
程序计数器
是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。通过改变这个计数器的值来获取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要以来这个计数器来完成。
JAVA虚拟机的多线程是通过线程轮流切换并分配处理器执行时间方式实现。在任何一个确定的时刻,一个处理器只会执行一条线程指令。因此,为了切换线程后恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间计数器互不影响,独立存储,这类内存区域称为“线程私有”内存。
如果线程执行的是JAVA方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是Native方法,这个计数器为空,这个内存区域是唯一虚拟机规范中没有规定任何OutOfMemoryError的区域。
虚拟机栈
线程私有,虚拟机栈的生命周期和线程的生命周期相同。虚拟机栈描述java方法执行时的内存模型:每个方法在执行的时候都会创建一个栈帧用户存储局部变量表、操作栈、动态连接和方法出口等信息。每个方法的执行过程就对应着一个栈帧从入栈到出栈的过程。
局部变量表存放各种基本数据类型(int、byte、char等)、对象引用(reference类型)、returnAddress类型(指向一条字节码指令地址)。
局部变量表所需要的空间在编译期间完成分配,进入方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。这里规定了两种异常情况:如果线程请求栈深度大于虚拟机允许的深度抛出StackOverFlowError;如果虚拟机允许动态扩展空间,当扩展无法申请到足够的内存则会抛出OutOfMemoryError。
本地方法栈
和虚拟机栈类似,虚拟机栈执行java方法,本地方法栈为虚拟机使用到的Native方法服务。
Java 堆
JAVA堆是被线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。所有对象实例以及数组都要在对上分配。
java堆是垃圾回收管理的主要区域。很多时候也被称为“GC堆”。java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。
方法区
线程共享的内存区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。也称为“非堆”。垃圾回收器在这个区域很少出现,但并不是非数据进入方法区之后就如永久代名称一样“永久”存在。这个区域回收的目标主要针对常量池的回收和对类型的卸载。
运行时常量池
属于方法区的一部分。Class文件中除了方法区所说的信息外还有一个常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。
Java虚拟机对Class文件的每一个部分都有严格的格式规定,每一个字节用于存储哪种数据都必须符合规范要求,这样才会被虚拟机认可、装载和执行。但是对于运行时常量池,Java虚拟机没有任何细节要求。一般情况,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池具有动态性,Java语言并不要求常量一定只能在编译时产生,在运行期间也可以将新的常量放入到常量池中,利用得比较多的是String类的intern() 方法。
对象访问
在java中,对象访问是如何进行的?
对象访问无处不在,最简单的行为也会涉及到java栈、java堆、方法区这三个最重要的内存区域。
如:Object obj = new Object();
Object obj 声明变量会反映到java栈的本地变量表中,作为一个reference类型数据。new Object定义会反映到java堆中,形成一块存储了Object类型的所有实例数据值的结构化内存。
reference类型在java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位。主流的访问方式有两种:使用句柄和直接指针。
-
使用句柄访问,java堆中会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和对象类型数据各自的具体地址信息。如图所示
直接指针访问方式。java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。如图所示
这两种方式各有优势,使用句柄方式最大好处就是reference中存储的是稳定的句柄地址,在对象被移动的时候(垃圾回收时,移动对象十分普遍)只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针方式访问的好处就是速度更快,节省了一次指针定位的时间开销,由于对象访问非常频繁,所以这种开销积少成多也是很可观的。
垃圾收集器与内存分配策略
可回收对象
引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器+1,失效时则-1.任何时刻计数器都为0的对象就是不可能再被使用的。则回收它。
优点:实现简单,判断效率高。
缺点:无法解决循环引用问题。
根搜索算法
通过一些列名为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则,证明此对象是不可用的。
java中GC Roots对象包括下面几种:
-
虚拟机栈,栈帧中的本地变量表中的引用对象
-
方法区中的静态属性引用对象
-
方法区中的常量引用对象
-
本地方法栈中JNI的引用对象
引用
JDK 1.2之后对引用概念进行了扩充分为:强引用、软引用、弱引用、虚引用。
-
强引用:在程序代码中普遍存在,类似Object obj = new Object()这种引用,只要强引用在,垃圾回收器就永远不会回收
-
软引用:用来描述一些还有用,但是非必需的对象。弱引用关联的对象在系统将要发生内存溢出之前,会将这些对象列入回收范围并进行二次回收
-
弱引用:描述非必需的对象,它的强度比软引用更弱,弱引用的对象只能生存到下一次GC发生之前
-
虚引用:最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成印象,也无法通过虚引用来获取对象实例。使用虚引用的唯一目的就是回收的时候会受到系统通知
方法区回收
方法区回收相对java堆回收来说效率低。
主要回收两部分内容:废弃常量和无用类。回收废弃常量比如一个字符串“abc”进入常量池中,但是当前系统没有任何一个String对象引用这个“abc”,也没有其他地方引用这个字面量,这就是一个废弃常量。
判断“无用类”的方式,必须同时满足:
-
该类所有的实例都被回收
-
加载该类的ClassLoader已被回收
-
该类对应的java.lang.Class对象没有任何地方引用,也无法在任何地方通过反射访问该类的方法
虚拟机可以对满足以上三个条件的类进行回收。
垃圾回收算法
标记-清除算法
最基础的手机算法,分为标记和清除两个阶段。首先标记处所有需要回收的对象,在标记完成之后统一进行回收。
缺点:
-
效率问题,标记和清除过程效率都不高
-
空间问题,标记清除之后会产生大量不连续的内存碎片。
复制算法
为了解决效率问题,诞生了复制算法。
它可以将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。当这块的内存使用完,就将还存活着的独享复制到另外一块上面,然后把使用过的内容空间一次清理掉。
缺点:
-
内存缩小为原来的一半
标记-整理算法
和标记-清除算法类似,第一个步骤标记需要回收的内存空间,然后将所有存活的对象向一端移动,使其连续避免造成内存碎片。然后直接清理掉端外内存空间。
分代收集算法
根据对象的存货周期的不同将内存划分为几块。一般分为新生代和老年代,这样就可以根据不同年代的特点采用最合适的收集算法。
篇二:类文件结构