简述:
说明:
简述:
虚拟机栈描述的是java内存模型,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法被调用到执行结束的过程,对应着一个栈帧从入栈到出栈的过程。
说明:(异常状况)
简述:
本地方法栈为JVM使用Native服务,虚拟机栈则为Java方法(字节码)服务。
说明:(异常状况)
简述:
Java堆是垃圾收集器管理的主要区域,即GC堆,从内存回收看,现在收集器基本都是分代收集算法。
Java堆分为:新生代和老年代。新生代其中包含Eden空间、From Survivor空间、To Survivor空间等。
说明:(异常状况)
简述:
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
说明:
运行时常量池:
简述:
直接内存不是JVM运行时数据区的一部分,也不是JVM规范定义中的内存区域。但是这部分内存也被频繁使用,也可能导致OutOfMemoryError异常。
在JDK 1.4中新引入了NIO(New Input/Output)类,是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为引用,去操作这块内存,可以避免在Java堆与Native堆来回复制数据。
在JVM中,java对象在内存中分为3块区域:
对象头包含两部分信息:
(1)存储对象自身运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据长度为32bit或64bit(32位或64位虚拟机),官方称为:Mark Word。
标志位 | 状态 | 存储内容 |
---|---|---|
01 | 未锁定 | 对象哈希码、对象分代年龄 |
00 | 轻量级锁定 | 指向锁记录的指针 |
10 | 膨胀(重量级锁定) | 指向重量级锁的指针 |
11 | GC标记 | 空,不需要记录信息 |
01 | 可偏向 | 偏向线程ID、偏向时间戳、对象分代年龄 |
(2) 类型指针即指向它的类元数据的指针,jvm通过其确定对象属于那个类的实例,并非所有虚拟机都必须在对象数据上保留类型指针,查找元数据信息不一定经过对象本身。Java数组对象头中还有一块用于记录数组长度的数据,普通的Java对象元数据信息能确定对象的大小,但数组的元数据无法确定数组大小。
实例数据是对象真正存储的有效信息即程序代码中定义的各种类型的字段内容,无论是父类继承的,还是子类定义的,都将记录下来。这部分存储顺序会受到JVM的分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响,相同宽度的字段总是被分配到一起。
从分配策略中可以看出,在父类中定义的变量尽会出现在子类之前,若CompactFields参数值为true(默认true),那么子类之中较窄的变量可能会插入到父类变量的空隙之中。
对齐填充并不是必然存在的,没有特别含义,仅仅起到占位符的作用。JVM自动内存管理系统要求对象起始地址必须是8字节的整数倍即对象大小必须是8字节的整数倍,对象头部分正好是8字节的倍数,因此当对象实例数据部分没有对齐时,则需要通过对齐填充来补全。
Java程序通过栈上的reference数据来操作堆上的具体对象,实际上reference类型在JVM中只规定了一个指向对象的引用,并没有规定是通过哪种方式去定位、访问堆中对象的具体位置,访问对象的方式取决于虚拟机JVM,目前主流访问方式主要分为两种:
(1)句柄访问方式;
(2)直接指针访问方式;
这两种对象访问方式各有优势:
访问方式 | 优势 | 劣势 |
---|---|---|
句柄访问方式 | GC时对象移动只改变实例数据指针,reference本身不需要改动 | 需要访问两次指针,获取实例数据和类型数据 |
直接指针访问方式 | 访问速度快,只访问一次指针 | GC时对象移动,reference需要改动 |
句柄访问方式是Java堆划分出来一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据(指针)和类型数据的具体地址信息(指针);
直接指针访问方式是reference中存储的就是对象地址,但Java堆对象布局就必须考虑如何放置访问类型数据的相关信息(指针);
JVM对堆进行GC前,需要判断对象是否存活(回收),那些已经“死去”(即不可能再被任何途径使用的对象)。
定义:
给对象中添加一个引用计数器,每当有一个地方引用,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优缺点:
(1)引用计数算法(Reference Counting)实现简单,判定效率高,大部分场景是一个不错算法,如微软公司发COM(Component Object Model)计数、FlashPlayer、Python语言等等;(优点)
(2)很难解决对象之间相互循环引用问题,如两个对象中的某个字段互相引用 testA.field = testB
和testB.field = testA;
(缺点)
通过一系列的“GC Roots”的对象作为起始点,从该节点开始向下搜索形成引用链(Reference Chain),当一个对象到GC Roots无任何引用链时(GC Roots与对象不可达),则说明此对象是不可用,可被回收。
在Java领域中,能作为*GC Roots对象的包括以下4种:
无论是引用技术算法、还是可达性分析算法都是通过“引用数量”和“引用链是否可达”,判定对象是否存活都与引用有关。
Test test = new Test()
;只要这个强引用还存在,GC收集器永远不会回收掉被引用的对象; 在可达性分析算法中,一旦java对象与GC Roots不可达时,也并非是必死的,要宣布一个对象真正被回收需要经历两个标记过程。
(1)第一个过程:若对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,则进行第一次标记且进行一次筛选(是否有必要执行finalize()方法)。没有必要执行的条件:当对象没有覆盖 finalize() 方法 或 finalize() 方法已经被虚拟机调用过。若被判定为有必要执行finalize()方法,则该对象将会被放置在F-Queue队列中,然后会由一个虚拟机自动建立的、低优先级的Finalizer线程去执行(JVM触发)。
说明:
(2)第二个过程:在第一个过程筛选标记后,对F-Queue队列中的对象进行小规模标记,然后进行回收。
public class OOMTest {
public static OOMTest obj = null;
public void test(){
System.out.println("test方法调用!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
System.out.println("对象this:"+ this);
obj = this;
}
public static void main(String[] args) throws InterruptedException {
obj = new OOMTest();
obj = null;
// 第一次调用gc,会调用finalize()方法,进行自救
System.gc();
long startTime = System.currentTimeMillis();
// 休眠2秒,gc里面的finalize()方法优先级较低
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
System.out.println("休眠时间:" + (System.currentTimeMillis() - startTime)/1000);
System.out.println("对象obj:"+obj);
if(obj != null){
obj.test();
}else{
System.out.println("gc 第一次已回收!");
}
// 第二次进行gc,不会再调用finalize()方法,无法进行自救
obj = null;
System.gc();
if(obj != null){
obj.test();
}else{
System.out.println("gc 第二次已回收!");
}
}
}
输出结果:
finalize method executed!
对象this:com.jvm.OOMTest@1bb3fcd
休眠时间:2
对象obj:com.jvm.OOMTest@1bb3fcd
test方法调用!
gc 第二次已回收!
Java虚拟机规范中确实说过不要求虚拟机在方法区中实现垃圾收集,并且方法区中垃圾收集性价比较低,堆中的新生代,一次垃圾回收能实现70%~90%的空间,永久代的垃圾收集效率远远低于堆中的回收。
永久代的垃圾收集主要分为两部分:
(1)废弃常量:没有其他地方引用这个字面量(常量池);
(2)无用的类:
最基础的收集算法:标记——清除(Mark-Sweep)算法,分为两个阶段即标记和清除。
(1)标记:首先标记出所有需要回收的对象;
(2)清除:将标记好的对象统一回收;
缺点:
(1)效率问题:标记和清除两个过程效率不高;
(2)空间问题:标记清除之后产生大量不连续的内存碎片,可能会导致分配大对象时无法分配到足够大的内存而触发GC。
为了解决效率问题,复制(copying)算法横空出世,将可用内存划分为大小相等的两块,每次使用其中的一块,当这块内存用完了就将活着的对象复制到另外一块儿,然后将使用过的这块内存全部清理掉。
优点:
每次对使用过程中的那块内存进行整个回收,内存分配时无需考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存,实现简单、运行高效(存活对象少时)。
缺点:
可用内存缩小了一半。
运用:
一般的JVM在新生代中使用复制算法,将新生代分为Eden空间和两块Survivor空间(8:1:1),每次使用Eden和其中一块Survivor,回收时将Eden和Survivor中还存活的对象一次性复制到另外未使用过的Survivor空间上。当复制过程中,用于活着的对象在新的Survivor空间无法存储时,会通过分配担保机制进入老年代。
复制算法在对象存活率较高时需要进行多次复制操作,效率就会急速下降,若其中一半内存不够用则需要额外的空间进行分配担保,保证在极端情况下也能存活,在老年代就不适用复制算法。
根据老年代的特点,“标记-整理”(Mark-Compact)算法随之诞生,标记过程与最基础算法——“标记-清除”中的标记过程一致,只是清除过程改为整理过程即将存活的对象向另外一端移动,然后直接清理掉端边界以外的内存。
Java堆分为新生代和老年代,然后根据其特点采用最适当的收集算法。
(1)新生代特点:垃圾收集时发现大批对象可回收,少量对象存活,采用 复制算法;
(2)老年代特点:对象存活率高,没有额外空间分配担保,采用 标记—整理算法 。