文章目录
- 1、运行时数据区域
- 1.1 程序计数器(线程私有)
- 1.2 JAVA虚拟机栈(线程私有)
- 1.3 本地方法栈
- 1.4 Java堆(线程共享)
- 1.5 方法区(线程共享)
- 1.6 直接内存(非运行时数据区域)
- 2、Java对象组成
- 2.1 对象头(Header)
- 2.2 实例数据(Instance Data)
- 2.3 对齐填充(Padding)
- 3、对象访问方式
- 3.1 句柄访问方式
- 3.2 直接指针访问方式
- 4、GC判断对象回收算法
- 4.1 引用计数算法(JVM未使用)
- 4.2 可达性分析算法(JVM使用)
- 5、对象引用种类
- 6、对象回收过程(两次标记)
- 7、方法区回收
- 8、GC回收算法(思想)
- 8.1 标记-清除算法
- 8.2 复制算法
- 8.3 标记-整理算法
- 8.4 分代收集算法(JVM常用)
1、运行时数据区域
- 程序计数器
- JAVA虚拟机栈
- 本地方法栈
- 堆
- 方法区
1.1 程序计数器(线程私有)
简述:
- 作用:当前线程所执行的字节码的行号指示器,字节码解释器通过改变计数器值选取下一条需要执行的字节码指令。
- 线程私有:Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间方式实现,在任意时刻,一个处理器只会执行一条线程中的指令。为了线程切换后恢复到正确的执行位置,每条先后才能都需要有一个独立的程序计数器,线程之间的计数器互不影响,独立存储。
说明:
- 线程正在执行java方法,此时计数器记录的是虚拟机字节码地址;
- 线程正在执行native方法,此时计数器记录值为空,此内存为JVM规范中没有规定任何OutOfMemoryError情况的区域。
1.2 JAVA虚拟机栈(线程私有)
简述:
虚拟机栈描述的是java内存模型,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法被调用到执行结束的过程,对应着一个栈帧从入栈到出栈的过程。
- 局部变量表:存放了编译器可知的八种基本数据类型(boolean\byte\char\short\int\float\long\double)和对象引用类型。
- 局部变量表中long和double占用2个局部变量空间(slot),其它数据类型占用1个,局部变量表所需内存空间在编译期间完成分配,在运行期间该方法局部变量表的大小不会改变。
说明:(异常状况)
- StackOverflowError:栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
- OutOfMemoryError:若虚拟机栈可扩展,当无法扩展无法申请到足够内存将抛出OutOfMemoryError异常。
1.3 本地方法栈
简述:
本地方法栈为JVM使用Native服务,虚拟机栈则为Java方法(字节码)服务。
说明:(异常状况)
- StackOverflowError(暂不赘述)
- OutOfMemoryError(暂不赘述)
1.4 Java堆(线程共享)
简述:
- 在虚拟机启动时创建,存放对象实例。
- 所有对象实例及数组都在堆上分配,但随着JIT编译器、逃逸分析技术、栈上分配、标量替换优化技术的发展,导致并非所有对象都在堆上分配。
Java堆是垃圾收集器管理的主要区域,即GC堆,从内存回收看,现在收集器基本都是分代收集算法。
Java堆分为:新生代和老年代。新生代其中包含Eden空间、From Survivor空间、To Survivor空间等。
说明:(异常状况)
- Java堆可以是物理上不连续的内存空间,只要逻辑连续即可。
- OutOfMemoryError:若堆中没有内存完成实例分配且堆无法再扩展,则会抛出OutOfMemoryError异常。
1.5 方法区(线程共享)
简述:
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
说明:
- 方法区不等价于永久代,GC分代收集扩展到方法区,或使用永久代来实现方法区而已。其他虚拟机是不存在永久代概念;
- 垃圾收集行为很少出现在方法区,该区域内存回收主要是对常量池和对类型的卸载,基本回收很少,效果不大理想。
- OutOfMemoryError:当方法区无法满足内存分配需求时,则将抛出OutOfMemoryError异常。
运行时常量池:
- 是方法区的一部分,Class文件中除了类的版本、字段、方法、接口等描述信息外,还有常量池,用于存储编译期生成的各种字面量和符号引用,是类加载后存到方法区的运行时常量池。
- 运行时常量池除了存储Class文件中描述的符号引用外,还将翻译出来的直接引用也存储在运营时常量池中。
- Class文件常量池具备动态性,运行期间也可能将新的常量放入运行时常量池,常见的便是String类的intern()方法。
1.6 直接内存(非运行时数据区域)
简述:
直接内存不是JVM运行时数据区的一部分,也不是JVM规范定义中的内存区域。但是这部分内存也被频繁使用,也可能导致OutOfMemoryError异常。
在JDK 1.4中新引入了NIO(New Input/Output)类,是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为引用,去操作这块内存,可以避免在Java堆与Native堆来回复制数据。
2、Java对象组成
在JVM中,java对象在内存中分为3块区域:
- (1)对象头(Header);
- (2)实例数据(Instance Data);
- (3)对齐填充(Padding)。
2.1 对象头(Header)
对象头包含两部分信息:
(1)存储对象自身运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据长度为32bit或64bit(32位或64位虚拟机),官方称为:Mark Word。
标志位 | 状态 | 存储内容 |
---|---|---|
01 | 未锁定 | 对象哈希码、对象分代年龄 |
00 | 轻量级锁定 | 指向锁记录的指针 |
10 | 膨胀(重量级锁定) | 指向重量级锁的指针 |
11 | GC标记 | 空,不需要记录信息 |
01 | 可偏向 | 偏向线程ID、偏向时间戳、对象分代年龄 |
(2) 类型指针即指向它的类元数据的指针,jvm通过其确定对象属于那个类的实例,并非所有虚拟机都必须在对象数据上保留类型指针,查找元数据信息不一定经过对象本身。Java数组对象头中还有一块用于记录数组长度的数据,普通的Java对象元数据信息能确定对象的大小,但数组的元数据无法确定数组大小。
2.2 实例数据(Instance Data)
实例数据是对象真正存储的有效信息即程序代码中定义的各种类型的字段内容,无论是父类继承的,还是子类定义的,都将记录下来。这部分存储顺序会受到JVM的分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响,相同宽度的字段总是被分配到一起。
从分配策略中可以看出,在父类中定义的变量尽会出现在子类之前,若CompactFields参数值为true(默认true),那么子类之中较窄的变量可能会插入到父类变量的空隙之中。
2.3 对齐填充(Padding)
对齐填充并不是必然存在的,没有特别含义,仅仅起到占位符的作用。JVM自动内存管理系统要求对象起始地址必须是8字节的整数倍即对象大小必须是8字节的整数倍,对象头部分正好是8字节的倍数,因此当对象实例数据部分没有对齐时,则需要通过对齐填充来补全。
3、对象访问方式
Java程序通过栈上的reference数据来操作堆上的具体对象,实际上reference类型在JVM中只规定了一个指向对象的引用,并没有规定是通过哪种方式去定位、访问堆中对象的具体位置,访问对象的方式取决于虚拟机JVM,目前主流访问方式主要分为两种:
(1)句柄访问方式;
(2)直接指针访问方式;
这两种对象访问方式各有优势:
访问方式 | 优势 | 劣势 |
---|---|---|
句柄访问方式 | GC时对象移动只改变实例数据指针,reference本身不需要改动 | 需要访问两次指针,获取实例数据和类型数据 |
直接指针访问方式 | 访问速度快,只访问一次指针 | GC时对象移动,reference需要改动 |
3.1 句柄访问方式
句柄访问方式是Java堆划分出来一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据(指针)和类型数据的具体地址信息(指针);
3.2 直接指针访问方式
直接指针访问方式是reference中存储的就是对象地址,但Java堆对象布局就必须考虑如何放置访问类型数据的相关信息(指针);
4、GC判断对象回收算法
JVM对堆进行GC前,需要判断对象是否存活(回收),那些已经“死去”(即不可能再被任何途径使用的对象)。
4.1 引用计数算法(JVM未使用)
定义:
给对象中添加一个引用计数器,每当有一个地方引用,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优缺点:
(1)引用计数算法(Reference Counting)实现简单,判定效率高,大部分场景是一个不错算法,如微软公司发COM(Component Object Model)计数、FlashPlayer、Python语言等等;(优点)
(2)很难解决对象之间相互循环引用问题,如两个对象中的某个字段互相引用 testA.field = testB
和testB.field = testA;
(缺点)
4.2 可达性分析算法(JVM使用)
通过一系列的“GC Roots”的对象作为起始点,从该节点开始向下搜索形成引用链(Reference Chain),当一个对象到GC Roots无任何引用链时(GC Roots与对象不可达),则说明此对象是不可用,可被回收。
在Java领域中,能作为*GC Roots对象的包括以下4种:
- (1)虚拟机栈(栈帧中的局部变量表)中引用的对象;
- (2)方法区中类的静态属性引用的对象;
- (3)方法区中常量引用的对象;
- (4)本地方法栈中JNI(Native方法)引用的对象。
5、对象引用种类
无论是引用技术算法、还是可达性分析算法都是通过“引用数量”和“引用链是否可达”,判定对象是否存活都与引用有关。
- (1)强引用(Strong Reference):强引用在程序代码中常见,如
Test test = new Test()
;只要这个强引用还存在,GC收集器永远不会回收掉被引用的对象; - (2)软引用(Soft Reference):描述一些还有用但并非必须的对象。对于软引用关联的对象,只要系统内存还足够,则不会回收。在系统将要发生内存溢出(OOM)之前,将会把这些对象列入回收范围之中进行第二次回收,若此次回收还没足够内存就会抛出内存溢出异常;
- (3)弱引用(Weak Reference):描述非必须对象,强度比软引用更弱,只能生存到下一次GC发生之前,一旦GC将会被会回收掉;
- (4)虚引用(Phantom Reference):幽灵引用或幻影引用,最弱的引用关系。对象是否存在虚引用,不会对其生存时间造成任何影响,也无法通过虚引用获取对象实例。虚引用的唯一目的就是在这个对象被GC后收到一个系统通知。
6、对象回收过程(两次标记)
在可达性分析算法中,一旦java对象与GC Roots不可达时,也并非是必死的,要宣布一个对象真正被回收需要经历两个标记过程。
(1)第一个过程:若对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,则进行第一次标记且进行一次筛选(是否有必要执行finalize()方法)。没有必要执行的条件:当对象没有覆盖 finalize() 方法 或 finalize() 方法已经被虚拟机调用过。若被判定为有必要执行finalize()方法,则该对象将会被放置在F-Queue队列中,然后会由一个虚拟机自动建立的、低优先级的Finalizer线程去执行(JVM触发)。
说明:
- (1)队列中的成员互不影响,各自执行,不会串联等待,防止某个对象执行finalize()方法缓慢或死循环导致队列其他对象永久性等待,最终导致内存回收系统崩溃。
- (2)finalize()方法是对象逃脱死亡的最后一次机会,后面GC对F-Queue中的对象进行第二次小规模标记,在第一次执行finalize()方法时与其他引用链上的任何一个对象建立关联即可自救(移除即将回收的集合)。
(2)第二个过程:在第一个过程筛选标记后,对F-Queue队列中的对象进行小规模标记,然后进行回收。
public class OOMTest {public static OOMTest obj = null;public void test(){System.out.println("test方法调用!");}@Overrideprotected 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@1bb3fcdtest方法调用!gc 第二次已回收!
7、方法区回收
Java虚拟机规范中确实说过不要求虚拟机在方法区中实现垃圾收集,并且方法区中垃圾收集性价比较低,堆中的新生代,一次垃圾回收能实现70%~90%的空间,永久代的垃圾收集效率远远低于堆中的回收。
永久代的垃圾收集主要分为两部分:
(1)废弃常量:没有其他地方引用这个字面量(常量池);
(2)无用的类:
- (1)堆中没有该类的任何实例;
- (2)加载该类的ClassLoader已经回收;
- (3)该类的Class 对象没有被其他地方引用,无法通过反射来访问该类方法;
8、GC回收算法(思想)
8.1 标记-清除算法
最基础的收集算法:标记——清除(Mark-Sweep)算法,分为两个阶段即标记和清除。
(1)标记:首先标记出所有需要回收的对象;
(2)清除:将标记好的对象统一回收;
缺点:
(1)效率问题:标记和清除两个过程效率不高;
(2)空间问题:标记清除之后产生大量不连续的内存碎片,可能会导致分配大对象时无法分配到足够大的内存而触发GC。
8.2 复制算法
为了解决效率问题,复制(copying)算法横空出世,将可用内存划分为大小相等的两块,每次使用其中的一块,当这块内存用完了就将活着的对象复制到另外一块儿,然后将使用过的这块内存全部清理掉。
优点:
每次对使用过程中的那块内存进行整个回收,内存分配时无需考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存,实现简单、运行高效(存活对象少时)。
缺点:
可用内存缩小了一半。
运用:
一般的JVM在新生代中使用复制算法,将新生代分为Eden空间和两块Survivor空间(8:1:1),每次使用Eden和其中一块Survivor,回收时将Eden和Survivor中还存活的对象一次性复制到另外未使用过的Survivor空间上。当复制过程中,用于活着的对象在新的Survivor空间无法存储时,会通过分配担保机制进入老年代。
8.3 标记-整理算法
复制算法在对象存活率较高时需要进行多次复制操作,效率就会急速下降,若其中一半内存不够用则需要额外的空间进行分配担保,保证在极端情况下也能存活,在老年代就不适用复制算法。
根据老年代的特点,“标记-整理”(Mark-Compact)算法随之诞生,标记过程与最基础算法——“标记-清除”中的标记过程一致,只是清除过程改为整理过程即将存活的对象向另外一端移动,然后直接清理掉端边界以外的内存。
8.4 分代收集算法(JVM常用)
Java堆分为新生代和老年代,然后根据其特点采用最适当的收集算法。
(1)新生代特点:垃圾收集时发现大批对象可回收,少量对象存活,采用 复制算法;
(2)老年代特点:对象存活率高,没有额外空间分配担保,采用 标记—整理算法 。