1、JVM内存模型
JVM只不过是运行在你系统上的另一个进程而已,这一切的魔法始于一个java命令。正如任何一个操作系统进程那样,JVM也需要内存来完成它的运行时操作。记住:JVM本身是硬件的一层软件抽象,在这之上才能够运行Java程序,也才有了我们所吹嘘的平台独立性以及“一次编写,处处运行”。
Java虚拟机在执行Java程序的过程中会把它说管理的内存划分为若干个不同的数据区域,如下面两图所示:
(1)程序计数器: 线程私有。程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行 指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复登记处功能都需要依赖这个计数器的值来完成。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。这类内存区域称为“线程私有”的内存。程序计数器,是唯一一个在java虚拟机规范中没有规定任何Out Of Memory Error的区域。
(2)Java虚拟机栈: 也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。平常我们把java分为堆内存和栈内存,其中的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
对于java虚拟机栈,有两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈在动态扩展时,无法申请到足够的内存,就会抛出OutOfMemoryError。
Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。
(3)本地方法栈: 线程私有。本地方法栈和虚拟机栈所发挥的作用非常相似,它们之间的区别主要是,虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务的,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈类似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
(4)Java堆: 所有线程共享。Java堆在虚拟机启动时创建,是Java虚拟机所管理的内存中最大的一块。Java堆的唯一目的就是存放对象实例和数组。
Java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。从内存回收的角度来看,由于现在的收集器大都采用分代收集算法,所以Java堆可以细分为:新生代和老年代;再细分一点:Eden空间、From Survivor空间、To Survivor空间等。从内存分配角度来看,线程共享的Java堆可以划分出多个线程私有的分配缓冲区。但是不管怎么划分,哪个区域,存储的都是对象实例。
Java堆物理上不需要连续的内存,只要逻辑上连续即可。如果堆中没有内存完成实例分配,并且也无法再扩展时,将会抛出OutOfMemoryError异常。
(5)方法区: 所有线程共享。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也有一个别名叫做Non-Heap(非堆),用于与Java堆区分。对于HotSpot虚拟机来说,方法区又习惯称为“永久代”(Permancent Generation),但这只是对于HotSpot虚拟机来说的,其他虚拟机的实现上并没有这个概念。相对而言,垃圾收集行为在这个区域比较少出现,但也并非不会来收集,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载上。
内存区域模型的总结如下图所示:
线程私有的区域:程序计数器、虚拟机栈、本地方法栈;
所有线程共享的区域:Java堆、方法区;
没有异常的区域:程序计数器;
StackOverflow Error异常:Java虚拟机栈、本地方法栈;
OutOfMemory Error异常:除程序计数器外的其他四个区域,Java虚拟机栈、本地方法栈、Java堆、方法区;
2)使用直接指针访问。此时reference中存储的就是对象的地址。如下图:
复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。这样可以使得每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针按顺序分配内存即可,实现简单运行高效。只是这种算法将内存缩小为原来的一半,代价有点儿高。
复制算法的执行过程如下图所示:
分代收集算法
当前商业虚拟机都采用这种算法。将Java 堆分为新生代和老年代,根据各代的特点选用前面介绍的三类收集算法中的某一种。例如新生代选择复制算法,老年代选择标记-整理算法。
6、基于分代策略的垃圾回收
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不可变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

当发生Minor GC时,除了将Eden区的非活动对象回收以外,还会把一些老对象也复制到Old区中。这个老对象的定义是通过配置参数MaxTenuringThrehold来控制的,如-XX:MaxTenuringThrehold=10,则表示如果这个对象已经被Minor GC回收过10次后仍然存活,那么这个对象在这次Minor GC后直接进入Old区。
JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
-XX:+PrintGC的输出形式如下: “` [GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs] “`
-XX:+PrintGCDetails 输出形式:
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!