自动内存管理机制
运行时数据区域
- 线程私有
-
程序计数器 Program Counter Register: 属于寄存器,记录当前线程执行指令地址(本地方法为空),通过改变其计数器的值获取下一条执行指令。 内存空间小,每个线程独有一份。因为Java多线程通过切换线程,轮流分配处理器,也就是说在一个时间内只有一条线程在执行,故而需要程序计数器记录之前执行的指令,以保证切换线程后能够恢复到之前执行的位置。
-
虚拟机栈 VM Stack:线程私有。描述了Java方法执行内存模型。 方法执行内存模型: 每个Java非本地方法调用都会创建一个栈帧,栈帧存储了局部变量表,操作数栈,动态链接,方法出口等信息。 入栈-------------->出栈 调用-->调用中-->调用完成
-
局部变量表:放有基本数据、引用(reference-对方地址的引用指针)、返回地址(returnAddress指令地址)
-
本地方法栈 Native Stack:Native方法调用所需的栈。
- 线程共享
-
堆 Heap: 所占内存空间最大,也是gc主要区域。 在虚拟机启动时创建,唯一目的只有存放对方实例。几乎所有对象都在堆中。 (1.7-堆可以分为新生代,老年代) 堆空间通过-Xmx -Xms设定(两者相等表示堆空间不会变动)
-
方法区 Method Area: 加载class文件后,信息保存在方法区中,其中包含类信息、常量、静态常量、符号引用等等 1.7以后Hotspot虚拟机将永久代移出方法区。
-
运行时常量池: 保存类加载后的字面量和符号应用等。
-
直接内存: NIO通过管道和缓存可以调用直接内存,从而避免频繁对堆内存的处理,提高性能。 避免虚拟机设定的总内存超出直接内存,从而导致OutOfMemoryError异常。
对象的创建
以new 关键字触对象的创建为例说明(其他有通过反射、子类加载前若父类未加载先加载父类等,类的加载过程,后续补充)
对象内存布局
确定属于哪个类,区别同一个类的实例,并且对象存储的数据和方法。更细致一层包含对象的回收,对象的锁等运行时的信息。
-
对象头: 自身信息: 哈希码、gc分代、锁、偏向锁等等信息 类型指针:对象指向它的元数据的指针,确定对象是哪个类的实例。 数组大小,只有Java数组需要。
-
实例数据:字段数据
-
填充:保证对象的大小必须是8字节的整数倍。
对象的访问定位-reference类型:
- 通过句柄访问对象 堆上要维护句柄池,对象改变只需要改变句柄池中的数据
- 通过指针访问对象 为对象地址,直接访问对象,一旦对象移动,reference需要改变。-HotSpot VM采用此方式。
垃圾收集器与内存分配策略
目的: 1.为了查找和解决内存溢出、内存泄漏等问题 2.为了提高垃圾收集性能。
垃圾收集:
0.垃圾回收什么?
1.什么时候需要垃圾回收?
2.怎么进行垃圾回收?
0.垃圾回收什么?
垃圾回收无用的对象、常量、静态常量甚至加载的类 其中:对象存储在堆中,常量、静态常量、类在方法区中。 即,垃圾回收主要回收堆和方法区中的内存。
- 如何判断对象无用,或者说‘死’了? 对象到GC Roots对象不可达,之间无引用链。通过可达性算法判断,从GC Roots为根节点,向下搜索,其走过的路径即位引用链。
GC Roots对象 虚拟机栈栈帧中引用的对象、本地方法栈中引用的对象、静态属性引用的对象、常量引用的对象
java虚拟机为了不将暂时不必须的对象一棍子打死,而细分了引用强度。
- 强引用: 普遍存在,不回收
- 软引用:有用但非必需,在内存即将溢出前进行回收
- 弱引用:非必须对象,活到一下次GC发生之前
- 虚引用:特点只是在回收时发送一个系统通知
1. 什么时候回收?
安全点-长时间执行的地方(区域块)产生:指令序列复用,如循环跳转,异常跳转、方法调用等 程序执行到安全点,线程主动运行到安全点(通过中断标志,线程轮询标志,将线程中断挂起),再进行回收。
2.如何进行垃圾回收?
方法区内存回收:
永久代:回收无用的类、废弃常量。 废弃常量:没有用的常量 无用的类:所有实例被回收,堆中无其实例;该类的类加载器被卸载回收;在任何地方,该类对应的java.lang。Class对象没有被引用,也无法通过反射访问该类的方法。
堆中内存回收
垃圾回收算法:
-
分代收集算法:商用虚拟机就堆中对象存活时间不同,而采用不同的回收算法。 新生代:复制算法 老年代:标记-整理算法 或 标记-清除算法。
-
标记-清除算法: 对可存活的对象进行标记,然后清除未标记的区域。 缺点: 1.容易造成不连续的内存碎片,后期可能找不到空间足够大的连续内存而触发垃圾回收 2.内存区域散乱,标记和清除效率不高。
-
复制算法: 对半分块,一块内存用完了,将在使用中的对象复制到另一块内存中,把使用的内存全部删掉。 优点:迅速、实现简单,运行高效。 缺点:为了保留可存活对象的存储空间,内存利用率无法达到100%
将新生代分为一个Eden、两个Survivor,默认内存空间大小比是8:1:1。由于新生代对象的回收率高达98%,回收时将eden和一个survivor中存活的对象复制到另外一块survivor区中,再清除之前的内存空间,当其内存空间不够,可依赖老年代空间进行分配担保。
- 标记-整理算法 对可存活对象进行标记,将标记的对象往一端移动(整理),将端外的内存空间直接清除。
垃圾回收器
非并发收集
单线程:
- Serial -新生代 复制算法
- Serial Old -老年代 标记-整理算法
- 优点:简单高效。单线程,无线程交互切换的开销,高效迅速。JVM Client模式下首选。能与CMS回收器配合使用。
- 缺点:存在Stop the World,所有线程停顿。一旦回收时间过长,难以接受。
多线程:
-
ParNew-Serial多线程版本,参与与Serial共用,Server模式下首选,因为其能与CMS收集器配合使用。
-
Parallel Scavenge -新生代,复制算法 吞吐量优先,自适应调节策略(系统会收集性能监控信息从而自适应调整GC停顿时间或者最大吞吐量。
-
Parallel Old-老年代,标记-整理算法,能够和Parallel Scavenge 配套使用。
吞吐量 = 用户运行代码时间/CPU总耗时
并发收集
垃圾收集与用户线程同时存在(一边制造垃圾一边回收):
CMS:-老年代 重视服务的响应速度,即要求最短停顿时间。 基于标记-清除算法
-
运作过程: 初始标记 - Stop the world 简单编辑GC Roots对象, 并发标记 - GC Roots Tracking(可达性分析)过程,用户线程也存在。 重新标记 - Stop the world 修正因并发标记导致的变动,时间比初始标记长,比并发标记短 并发清除 -清除标记的对象,用户线程存在。
-
优点:低停顿,并发收集。
-
缺点:
- 对cpu资源敏感--收回占用资源,影响服务响应,尤其在低核心的情况下,对cpu占用率较高,并发标记和并发清除的时间较长,影响更大。
- 会因为并发清理阶段产生浮动垃圾,内存空间不足的情况下会触发Full GC,出现线程停顿。因CMS并发清理,需要为用户线程预留内存空间,一旦预留空间不出,会出现Concurrent Mode Failure,jvm会临时启用Serial Old收集器进行老年代垃圾回收,从而出现Stop the world所有线程停顿。
- 标记-清除算法的缺点,会出现不连续的内存空间,导致出现大对象分配内存的时候触发Full GC。可以设置-XX:CMSFullGCsBeforeCompaction =n 参数,在执行n次不压缩的Full GC后,进行碎片整理(默认该参数为0,即每次Full GC时都进行碎片整理)
G1:
-
并发收集
-
分代收集:堆内存没有分为新生代和老年代,将堆分为多个region,所谓的新生代和老年代都是一部分region集合。region有一个与之对应的Remember Set维护,引用的对象处于不同region中(老年代的对象引用了新生代的对象),通过CardTable把相关引用信息记录到被引用对象所属的Region的Remember Set中。
-
空间整合:不存在大量不连续得碎片。 低停顿,可预测的停顿(可预测时间模型,能够明确指定在M秒的时间内,收集垃圾的时间不得超过N秒)。
-
运作过程:
- 初始标记-线程停顿,时间小
- 并发标记-可达性分析找出可存活得对象
- 最终标记-将并发中用户线程产生
- 塞选回收