JVM内存简介
JVM执行Java程序的过程:Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。
在上述过程中,JVM会用一段空间来存储执行程序期间需要用到的数据和相关信息,这段空间就是运行时数据区(Runtime Data Area),也就是常说的JVM内存。
JVM内存划分
JVM内存分为线程私有数据区和线程共享数据区两大类:
- 线程私有数据区包含:程序计数器、虚拟机栈、本地方法栈
- 线程共享数据区包含:Java堆、方法区(内部包含常量池)
1. 程序计数器(Program Counter Register):
- 是当前线程所执行的字节码的行号指示器。
- 如果线程正在执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果线程正在执行的是一个Native方法,那么计数器的值则为空。
- 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,因此它是线程私有的内存。
2. Java虚拟机栈(Java Virtual Machine Stacks):
- 每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
3. 本地方法栈(Native Method Stack)
- 是虚拟机使用到的Native方法服务
4. Java堆(Java Heap)
- 用于存放几乎所有的对象实例和数组。
- 是垃圾收集器管理的主要区域,也被称做“GC堆”。
5. 方法区(Method Area)
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可选择不实现GC。
6. 运行时常量池(Runtime Constant Pool)
- 相对于Class文件常量池的一个重要特征是具备动态性,体现在并非只有预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
- 是方法区的一部分,会受到方法区内存的限制。
GC
引用的种类:
- 强引用(StrongReference)
- 具有强引用的对象不会被GC;
- 即便内存空间不足,JVM宁愿抛出
OutOfMemoryError
使程序异常终止,也不会随意回收具有强引用的对象。
- 软引用(SoftReference)
- 只具有软引用的对象,会在内存空间不足的时候被GC,如果回收之后内存仍不足,才会抛出OOM异常;
- 软引用常用于描述有用但并非必需的对象,比如实现内存敏感的高速缓存。
- 弱引用(WeakReference)
- 只被弱引用关联的对象,无论当前内存是否足够都会被GC;
- 强度比软引用更弱,常用于描述非必需对象。
- 虚引用(PhantomReference)
- 仅持有虚引用的对象,在任何时候都可能被GC;
- 常用于跟踪对象被GC回收的活动;
- 必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
- 引用计数算法:
- 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
- 缺点:在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题。如:
MyObject myObject1 =newMyObject(); MyObject myObject2 =newMyObject(); myObject1.ref = myObject2; myObject2.ref = myObject1; myObject1 =null; myObject2 =null;
当执行myObject1 =null的时候,由于还有myObject2,myObject1.ref计数器不为0,myObject1不能被回收;同理myObject2=null的时候,也不能被回收。
在循环链表的时候,会导致链表永远不能被回收。
- 可达性分析法:
- 通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图,Object1-4是可达的,Object5-6不可达。
在Java中,可作为GC Root的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
垃圾收集算法:
- 分代收集算法
- 根据对象存活周期的不同,将Java堆划分为新生代和老年代,并根据各个年代的特点采用最适当的收集算法。
- 新生代:大批对象死去,只有少量存活。使用『复制算法』,只需复制少量存活对象即可。
- 老年代:对象存活率高。使用『标记—清理算法』或者『标记—整理算法』,只需标记较少的回收对象即可。
- 是当前商业虚拟机都采用的一种算法。
- 根据对象存活周期的不同,将Java堆划分为新生代和老年代,并根据各个年代的特点采用最适当的收集算法。
- 复制算法
- 把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象『复制』到另外一块上面,再将这一块内存空间一次清理掉。
- 优点:每次都是对整个半区进行内存回收,无需考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
- 缺点:每次可使用的内存缩小为原来的一半,内存使用率低。
- 改进:有研究表明新生代中的对象98%是朝生夕死的,因此没必要按照1:1来划分内存空间,而是分为一块较大的Eden空间和两块较小的Survivor空间,在HotSpot虚拟机中默认比例为8:1:1。每次使用Eden和一块Survivor,回收时将这两块中存活着的对象一次性地复制到另外一块Survivor上,再做清理。可见只有10%的内存会被“浪费”,倘若Survivor空间不足还需要依赖其他内存(老年代)进行分配担保。
- 标记-清除算法
-
- 首先『标记』出所有需要回收的对象,然后统一『清除』所有被标记的对象。
- 缺点:『标记』和『清除』过程的效率不高;空间碎片太多,『标记』『清除』之后会产生大量不连续的内存碎片,可能会导致后续需要分配较大对象时,因无法找到足够的连续内存而提前触发另一次GC,影响系统性能。
- 标记-整理算法
-
- 首先『标记』出所有需要回收的对象,然后进行『整理』,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
- 优点:即没有浪费50%的空间,又不存在空间碎片问题,性价比较高。
- 一般情况下,老年代会选择标记-整理算法。
类加载到内存的过程
- 加载(Loading):
- 定义此类的二进制字节流
- 将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,它将作为程序访问方法区中的这些类型数据的外部接口。
- 验证(Verification): 是连接阶段的第一步,为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备(Preparation):
- 为类变量分配内存:因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中。
- 设置类变量初始值:通常情况下零值。
- 解析(Resolution):连接阶段的最后一步。虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用(Symbolic References):以一组符号来描述所引用的目标。与虚拟机实现的内存布局无关,即使各种虚拟机实现的内存布局不同,但是能接受符号引用都是一致的。
- 直接引用(Direct References):可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。与虚拟机实现的内存布局相关。
- 初始化(Initialization):执行类构造器
<clinit>()
的过程 - 使用(Using)
- 卸载(Unloading)
JAVA内存模型
高速缓存:为了平衡计算机的存储设备与处理器的运算速度之间几个数量级的差距,引入一层高速缓存(Cache)来作为内存与处理器之间的缓冲。
缓存一致性(Cache Coherence)的问题:每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory),当多个处理器的运算任务都涉及同一块主内存区域时,就可能导致各自的缓存数据不一致。解决办法就是需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。
同理,为了屏蔽各种硬件和操作系统的内存访问差异,Java内存模型(Java Memory Model,JMM)如下:
- 主内存(Main Memory):所有变量的存储位置。直接对应于物理硬件的内存。
- 工作内存(Working Memory):虚拟机可能会让工作内存优先存储于寄存器和高速缓存中。
- 交互协议:
- 8种操作:锁定(
lock
)、解锁(unlock
)、读取(read
)、载入(load
)、使用(use
)、赋值(assign
)、存储(store
)、写入(write
)***read操作读取变量到线程以便随后load,load操作将工作读取的变量载入内存的变量副本中;store对应read,write对应load - 不允许
read
和load
、store
和write
操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。 - 不允许一个线程丢弃它的最近的
assign
操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。 - 不允许一个线程无原因地,即没有发生过任何
assign
操作就把数据从线程的工作内存同步回主内存中。 - 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(
load
或assign
)的变量,即对一个变量实施use
、store
操作之前必须先执行过了assign
和load
操作。
- 8种操作:锁定(