jvm 深入理解自动内存分配与垃圾回收
Posted zzlove2018
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm 深入理解自动内存分配与垃圾回收相关的知识,希望对你有一定的参考价值。
要想了解jvm自动内存分配,首先必须了解jvm的运行时数据区域,否则如何知道在哪里进行自动内存分配,如何进行内存分配,回收哪里的垃圾对象?
jvm运行时数据区:程序计数器,虚拟机栈,本地方法栈,方法区,堆
程序计数器:由于程序指令是一条一条顺序执行,一条执行完之后必须知道下一条该执行那条指令,那么程序计数器就是来记录下一条指令的地址,如果调用本地方法,则程序计数器记录空值,还有由于java线程由cpu调度并发执行,所以程序计数器也有助于线程状态的恢复,程序计数器如果线程共享,在频繁的线程调度下保持同步影响效率,所以程序计数器是线程私有的,且程序计数器是运行时内存区域中唯一一处不会产生OutOfMemoryError错误
虚拟机栈:线程私有的内存区域,存放栈帧信息,每一个方法调用返回就对应着一个栈帧进栈出栈的过程,栈顶的栈帧表示当前执行方法的信息,栈帧信息主要包括:局部变量表,操作数栈,动态链接,返回地址等信息,局部变量表用于存放方法中出现的变量,局部变量表第一个为this,表示当前引用对象,表大小在编译时可知,每一个method_ref有一个max_locals属性,表示最大的局部变量表大小,操作数栈用于存放方法中操作数和直接结果,操作数栈一次最多能操作栈顶前两位操作数,动态链接用于指向该栈帧对应的方法区常量池中的引用,为了支持方法调用中的动态链接,返回地址信息用于存储方法执行完时方法的返回地址,即调用者的地址,方法正常结束和异常结束都会返回至调用者地址,但是方法正常结束可能会返回返回值,但异常结束不会有返回值,虚拟机栈可能出现的错误有OutOfMemoryError和OutOfStackError
本地方法栈:线程私有的内存区域,与虚拟机栈类似,唯一不同的是存放本地方法对应的栈帧信息
方法区:线程共享的内存区域,存放类信息,静态变量,常量,编译后程序代码等信息,类加载器在类加载阶段就是将字节码文件的静态结构转换至方法区的运行时结构,包括运行时常量池,存放字面量及符号引用,方法区也可能会产生OutOfMemoryError错误
堆:线程共享的内存区域,存放对象实例,垃圾回收的主要工作区域,为了方便内存自动分配和垃圾回收,将堆区分为新生代和老年代,新生代又可以详细分为Eden区,From survivor,To survivor区。堆也可能会发生OutOfMemoryError错误
主要的内存异常:内存泄漏,内存溢出,内存泄漏是指无法回收无用的对象,而无用的对象始终占用着内存空间,使cpu无法分配此内存空间,造成内存泄漏。内存泄漏严重的话最终将会导致内存溢出,内存溢出是指无法开辟足够的内存空间来满足对象的创建
自动内存分配机制:一个对象在实例化之前,对象的大小就已经确定,正确来说,对象的大小编译时就已经确定,最后会详细介绍如何确定对象大小。首先会根据内存是否规整采取不同的内存分配算法,如果内存规整,则会根据指针碰撞法来为对象划分内存空间,如果内存不规整,即内存由不连续的内存碎片构成,则需要维护一个内存的空闲列表,再根据空闲列表为对象分配内存空间。而内存是否规整则取决于垃圾回收算法是否包含整理操作,一般的,复制算法,标记-整理算法自带整理操作,可采用指针碰撞法,而标记-清除算法不含有整理操作,只能用空闲列表法为对象分配内存。因为在多线程的环境下,内存空间又是共享资源,不采取措施的话可能会造成多个线程抢占同一块内存区域,造成内存分配错乱,一般来说有两种解决方案,一种是在线程进行内存空间分配时进行cas失败重试措施来解决多线程竞争问题,另一种是为每个线程分配一块线程本地分配缓冲TLAB,每个线程在进行内存空间申请的时候在自己的分配缓冲区域进行申请,多个线程不会发生错乱。内存分配策略:对象优先在新生代Eden区分配,大对象直接进入老年代,长期存活的对象进入老年代,在survivor区中某个同一年龄的对象大于等于整个survivor区域的一半,则survivor区中大于此年龄的对象进入老年代
对象内存布局:已经为对象分配了一块内存区域,接下来就是对此内存区域根据对象信息进行数据填充了。对象内存布局有对象头,对象体,对齐填充,因为每个对象的内存空间要保证8字节的整数倍大小,当对象头对象体总共空间大小不是8字节整数倍时,需要填充某些字节。至于为什么对象大小要为8字节整数倍我也不知道?很多地方只是说hotspot虚拟机实现的规范要这样,至于为什么,怕是要问hotspot开发人员了,或许hotspot虚拟机是把8字节当做一个整体作为一个单位? 对象头信息:包含两至三部分数据,第一部分包括对象的哈希值,锁标志,是否采用偏向锁,线程ID等,这部分数据成为 mark word,对象锁一般四种状态:无锁,偏向锁,轻量级锁,重量级锁,随着多线程对对象的使用竞争,锁强度逐渐增强。第二部分是对象的类型指针,指向方法区对应的Class对象,用于找到对象所属类的元数据信息,对象类型指针在虚拟机规范中并不一定必须存在,因为对象头可以不包含类指针信息,如果采用句柄的对象访问方式,类指针信息可以存放在句柄中,但hotspot虚拟机中采用直接指针的对象访问方式,所以对象头一般包含类型指针,第三部分可有可无,如果对象是数组,第三部分表示数组长度,4字节大小。对象体信息:存放对象的实例数据,包含从父类继承来的属性值
对象内存分配一般过程:检查类型是否加载,进行内存分配(如果设置TLAB则采用内存分配缓冲为对象开辟内存空间,若没有则采用cas失败重试机制),为对象实例属性赋默认零值,int类型为0,boolean类型为false,引用类型为null,设置对象头信息,完成过后在虚拟机看来对象已经创建完毕,而对于开发人员来说,创建对象才刚刚开始,因为还没进行<init>方法,接下来执行<init>方法根据实例属性赋值及代码块对对象赋值
自动垃圾回收机制:自动垃圾回收中几个主要问题:哪些对象需要标志为垃圾对象?何时对垃圾对象进行回收操作?采用何种算法进行垃圾回收操作?
判断垃圾对象的算法:引用计数法,可达性分析法(根搜索算法),引用计数法比较好理解,每个对象都自带一个计数器,每当有一个对象引用自己时,计数器加1,当对象放弃引用自己时,计数器减1,当计数器值为0时则代表此对象为垃圾对象,虽然此算法好理解,也比较容易实现,但是有一些问题,如果两个对象没有其他对象引用,却双方互相引用对象,即无用对象循环引用导致内存泄漏。大部分虚拟机采用可达性分析算法,先将一些对象作为roots,在roots引用链上对象表示有用,没有在roots引用链上对象表示无用,可以判为垃圾对象,虚拟机规定将虚拟机栈变量引用的对象,本地方法栈变量引用的对象,方法区常量池引用的对象,方法区静态变量引用的对象可以作为roots对象
何时进行垃圾回收:当内存空间无法满足为新对象开辟新空间时进行垃圾回收操作,垃圾回收操作包括Minor GC,Full GC,Minor GC主要负责新生代垃圾对象的回收,Full GC主要负责整个共享内存区域的垃圾对象的回收,包括方法区,当新生代Eden区域内存空间无法满足新对象创建时,且新对象不是大对象,不会直接进入老年代,这时会触发Minor GC,由于Minor GC采用复制算法,Full GC采用标记-整理算法或标记-清除算法,老年代作为新生代的分配担保,因为新生代to survivor区无法满足存活对象,所以需要老年代分配担保存放多余的存活对象,如果老年代没有足够的空间进行分配担保,则会先触发Full GC,用于Full GC是整个共享内存区域的垃圾回收,所以一般伴随着一次Minor GC,不仅在分配担保不成时会触发Full GC,当新生代对象进入老年代而老年代没有足够空间时也会触发Full GC,Full GC主要进行老年代对象的回收,其次还会进行方法区垃圾对象的回收,主要是回收废弃常量和无用的类,判断废弃常量很简单,只要这个系统没有对象在引用此常量时,就会被判为废弃常量,判断无用的类比较苛刻,需要满足以下三个条件:1.该类所有实例对象都已经被回收 2.没有任何对象通过反射访问该类 3.执行该类加载操作的类加载器也已经被回收
垃圾回收算法:复制算法,标记-清除算法,标记-整理算法
复制算法:将内存区域分为两块,一半为空,一半用于存储对象,当进行垃圾回收时,只需要改变不是垃圾对象的地址指针为空区域,自带整理过程,新内存地址一字排开。然后清空另一半区域,即全部回收。但存活对象已经改变地址不会被回收,如果空间为1:1的话,则也不需要分配担保。缺点:内存空间利用率不高
标记-清除算法:垃圾回收过程分为标记和清除两个阶段,先根据可达性分析算法标记出所有垃圾对象,然后清除所有垃圾对象。缺点:标记和清除两个过程的效率都不高,而且垃圾回收过后由于没有整理操作,容易产生内存碎片,产生内存碎片之后,会更容易触发垃圾回收操作,恶性循环
标记-整理操作:垃圾回收过程分为标记和整理两个阶段,先标记出所有垃圾对象,然后改变存活对象的内存指针,清空剩余垃圾对象
jvm根据新生代对象和老年代对象不同特性采用分代收集算法,由于新生代对象存活率低,大多用完就不用,判为垃圾对象,所以采用复制算法,但复制算法不是1:1,而是9:1,具体来说是Eden:From survivor:To survivor=8:1:1,因为新生代对象存活率极其低,不需要一半空间来存放存活对象,十分之一差不多就足够,实在不够的话还可以用老年代作为分配担保,用于极少对象存活,所以复制算法中复制操作也少了,效率很高,由于老年代大多数都是大对象,存活率高的对象,采用复制算法将有大量复制操作,而且复制操作都将对大对象进行复制,而且没有内存为老年代作分配担保,若硬要采用复制算法,那不仅老年代内存空间利用率低,而且回收效率也会变低,一般老年代采用标记-整理算法或标记-清除算法,快速清除大对象
对象大小计算:对象大小根据根据jvm位数不同而不同,而且在64位虚拟机中,为了提高空间利用率,可能会采取压缩处理,这项处理也会对对象大小造成影响。在32位虚拟机中引用类型用4字节来表示,且对象头 mark word部分空间大小为4字节,所以对象头总共大小为8字节。在64位虚拟机中,未采用压缩处理的话,引用类型为8字节,且对象头 mark word部分空间大小为8字节,所以总共大小为16字节,注意:只有在64位虚拟机情况下,才会去考虑压缩处理,在压缩处理情况下,主要讲引用类型8字节压缩为4字节,所以压缩情况下对象头为12字节,上述如果对象表示数组的话,再加4字节数组长度
对象的字段布局:按照对象属性声明顺序且结合long/double 8字节,int/float 4字节,short/char 2字节,byte/boolean 1 字节,reference顺序进行字段布局,且对下一个字段长度进行对齐(这里跟网上其他说法不一致,测试得到),如果64位采用压缩处理的话 reference
4字节,且由于对象头为12字节,如果存在long/double类型属性,则优先将一个int/float填充4字节为long/double8字节整数倍,若没有则填充short/char/boolean/byte填充4字节为long/double类型的整数倍,若仍没有填满,则空字节对齐填充,注意首4字节填充过程中也要保证对下字段对齐
例子:采用压缩的64位虚拟机 class Person{byte a;short b;long c;Person d} 则首先12字节对象头,然后两字节short类型填充4字节,没有填满,则继续byte类型填充,仍然没有填充满,则空字节继续填充至16字节,然后8字节long类型,最好4字节Person引用类型,即0-11对象头 12-13 short类型 14 byte类型 15空字节填充 16-23 long类型 24-27 Person引用类型
以上是关于jvm 深入理解自动内存分配与垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章