本篇文章主要介绍一下jvm的内存管理机制,包括内存区域和垃圾收集相关内容。
1、jvm运行时数据区域包括方法区(Method Area)、堆(Heap)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)和程序计数器(Program Counter Register)。
①程序计数器,可理解为当前线程所执行的字节码的行号指示器,每一个线程都会有一个独立的程序计数器,线程之间独立存储、互不影响。
②虚拟机栈,每个方法在执行的同时都会创建一个栈帧用户存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法的调用到完成对应着虚拟机栈中入栈到出栈的过程。局部变量表存放各种基本数据类型、对象引用和returnAddree类型(指向一条字节码指令的地址)。若线程请求栈深超过虚拟机允许的深度,会抛出StackOverflowError;虚拟机扩展时无法申请足够内存,会抛出OutOfMemoryError。
③本地方法栈,类似于虚拟机栈,虚拟机栈为java方法服务,而本地方法栈为native方法服务。(Sun HotSpot直接将本地方法栈和虚拟机栈合二为一)
④Java堆,是虚拟机内存管理中最大的一块,所有线程共享,存放对象实例。Java堆是垃圾收集器管理的主要区域,又称“GC堆”(Garbage Collected Heap)。从垃圾回收的角度,可以划分为老年代和新生代,再细致又可划分Eden空间、From Survivor空间,To Survivor空间,从内存分配又可划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer),但存储的还都是对象实例。
⑤方法区,线程共享,存储类信息、常亮、静态变量、即时编译器编译后的代码等数据。HotSpot之前实现上又称方法区为“永久代”,其实更容易出现内存溢出问题(对此区域完全不收集),或逐步改为Native Memory实现,并且将字符串常量池移出(Java技术——你真的了解String类的intern()方法吗,jdk1.7+移到Java Heap)。此外趁机补充一下内存泄漏和内存溢出,内存泄漏:应用使用资源之后没有及时释放资源,导致应用内存中持有的了不需要的资源;内存溢出:应用内存已经不能满足正常的使用,堆栈已经达到系统设置的最大值,导致崩溃。
⑥运行时常量池,用户存放编译期各种字面量和符号引用,运行期间也可将常量放入池中,例如String类的intern()方法。
⑦直接内存(Direct Memory),并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。jdk1.4引入NIO,基于通道(Channel)与缓冲区(buffer)的I/O方式,通过Native库函数直接分配堆外内存,通过Java堆中的DirectByteBuffer对象作为引用进行操作,避免java堆和Native堆来回复制数据,提高性能。
2、对象的创建。
①遇到new指令,在常量池中寻找类的符号引用,检查是否被加载、解析和初始化过,否则进行加载②为新对象分配内存空间③初始化0值(不包括对象头),保证对象不赋初始值可直接使用④设置对象头(Object Header),例如类的元数据、哈希码、分代年龄、是否启用偏向锁等⑤执行init()方法
步骤二为对象分配内存空间有两种方式:“指针碰撞”和“空闲列表”,依据内存规整性做分配,使用Serial(串行收集器)、ParNew(Serial多线程版本)等带有compact(聚集)过程的收集器,系统采用指针碰撞(一边存放对象,一边空闲,每新增一个对象,指针向空闲区域挪动);而使用CMS(Concurrent Mark Sweep 并发标记清理)基于Mark-Sweep算法收集器,通常采用空闲列表法(在分配的时候从列表中找到一块足够大的空间划分给对象)。在并发过程中,虚拟机分配内存有两种策略,对分配内存空间动作进行同步处理,CAS+失败重试;把内存分配动作按线程划分在不同空间进行操作,即本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),通过-XX:+/-UseTLAB设置。
3、对象的内存布局
在HotSopt虚拟机中,对象在内存中的存储布局可分为对象头、实例数据和对齐填充。对象头包含两部分内容①对象自身运行时数据,如哈希码、GC分代年龄、锁状态标准、线程持有的锁、偏向线程ID、偏向时间戳,此部分数据又称“Mark Word”,另一部分,存放类型指针,即类元数据指针,用于判断对象属于哪个类的实例。 实例数据,即各种字段内容、继承信息,默认分配策略是相同宽度的字段分配到一起。对齐填充,对象起始地址必须是8字节的整数倍。
4、对象访问定位
使用对象,即通过栈上的reference数据类操作堆上的具体对象,目前reference存储句柄(handle)地址或直接指针。句柄方式,堆中会分出一块内存作为句柄池,reference存放句柄地址,当对象移动是改变句柄中实例数据的指针,reference本身不必修改;直接指针方式,访问更快,避免一次指针定位时间开销,需要修改reference,HotSpot使用直接指针方式。
5、判断对象“存活”的办法
①引用计数算法:给对象添加一个引用计数器,有一个地方就+1,引用失效就-1,任何时刻,计数为0的对象不可再被引用。jvm没有选用该种算法,该种算法不能解决循环引用问题。
②可达性分析算法:通过一系列“GC Roots”对象作为起始点,从节点向下搜索的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任务引用链,证明不可用。能够作为GC Roots的对象有:虚拟机栈(本地变量表)中引用的对象、方法区中类静态属性应用的变量、方法区中常量引用的对象、本地方法栈JNI(一般是Native)引用的对象。
③引用:当一个对象处于“食之无味弃之可惜”时,以上两种方法就无法判断。在jdk1.2之后,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)
强引用:在虚拟机中普遍存在,类似于Object obj = new Object(),只要强引用在,就不会被回收。
软引用:描述有用但并非必需的对象。提供SoftReference类来实现。系统在发生内存溢出之前,会把对象列进回收范围中进行第二次回收,如果回收后还没有足够内存,则抛出内存溢出异常。
弱引用:描述非必需对象,比软引用更弱。提供WeakReference类实现。只能生存到下一次垃圾收集发生之前。收集器工作一定会回收弱引用对象。
虚引用:又称幽灵引用或幻影引用,无法通过虚引用来取得实例。提供PhantomReference。使用虚引用的目的就是在该对象被收集器回收收到一个通知
④java finalize()方法,当对象没有覆盖finalize()方法或finalize()已经被调用,则视为“没有必要执行”,避免通过finalize方法拯救对象。
6、回收方法区,主要回收废弃常量和无用的类。系统没有其他对象引用常量,就会将该常量清理出去。无用的类需要满足:①所有的实例已经被回收②加载该类的ClassLoader已经被回收③该类的Class对象没有被引用,无法在任何地方通过反射访问该类。满足以上条件,可进行回收(应用在大量使用反射、动态代理、CGLib,动态生成JSP、OSGi等功能)。
7、垃圾收集算法
①标记-清除算法(Mark-Sweep),标记和清除两个过程效率不高,容易产生内存碎片。
②复制算法,将内存分两半,每次只用其中一半,gc时,将存活的移到另一半,对之前进行清理。代价太高,内存缩小一半。IBM专门研究,将内存(新生代)分配成8:1:1即Eden,form survivor,to survivor,每次只用Eden和一个survivor,清理时将存活的对象移到另一个survivor,空间不足,则依赖老年代内存。
③标记-整理算法(Mark-Compact),跟据老年代特点设计,跟标记-清除类似,不过标记之后,将存活对象往一端移动,清除端边界以外的内存。
④分代收集(Generational Collection),根据对象存活周期不同,划分为几块,选择合适的收集算法。
参考:
《深入理解Java虚拟机》