对于Java程序员,在虚拟机自动内存管理机制的帮助下,不需要再为每一个操作写配对的释放资源操作,不容易出现内存泄露和内存溢出问题。加深对Java虚拟机的理解,有助于在发现问题时精准定位问题,排查错误因素,写出更健壮的代码。
一、虚拟机内存
Java虚拟机在执行Java程序时,会把它所管理的内存划分成若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。从粗略的角度来说,可以分为堆和栈。堆为线程公有,栈为线程私有。
1.1 程序计数器
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器。各条线程之间的计数器互不影响、独立存储,称这类内存区域为“线程私有”的内存。
1.2 Java虚拟机栈、本地方法栈
Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
1.3 堆、方法区
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存:所有的对象实例以及数组都要在堆上分配。
方法区(别名非堆:Non-Heap)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.4 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。——用于存放类加载后的编译信息
1.5 直接内存
2、对象
2.1 对象的创建
①运行时常量池-检查类是否已被加载
②为新生对象分配内存
指针碰撞 Bump the Pointer :假设Java堆中的内存是绝对规整的,内存区分为可用和已用两部分,中间用指针分隔,作为分界点的指示器。分配内存就是将指针向空闲区域移动与对象大小相等的距离。
空闲列表 Free List:Java堆中的内存并不规整,虚拟机维护一个列表,记录哪些内存块是可用的,在分配的时候从列表上找到一个足够大的空间划分给对象实例,并更新列表上的记录。
CAS/失败重试 Compare And Swap:保证分配内存的并发安全
TLAB:本地线程分配缓冲(Thread Local Allocation Buffer):每个线程在Java堆中预先分配一小块内存,哪个线程要分配内存,就先在TLAB上分配,只有当TLAB用完并分配新的TLAB时,才需要同步锁定
③对对象进行必要的设置
将对象信息存储在对象的对象头(Object Head)之中。
2.2 对象的内存布局
对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
①对象头:
Mark Word:存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,若对象为一个Java数组,则对象头中还必须有一块用于记录数组长度的数据。
②实例数据
③补齐
虚拟机大小必须是8字节的整数倍,不规整的
3、运行测试
1、使用IDEA,设置虚拟机参数: -Xss参数,减小栈内存容量。
测试程序:
public class JavaVMStackSOF {
private int stackLength = 1;
private void stackLeak(){
stackLength ++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oom.stackLeak();
}catch (Throwable e){
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
输出结果:
抛出StackOverflowError异常,异常时输出的堆栈深度相应缩小。
二、垃圾回收策略
1、引用
判断引用是否存在,用于判断对象是否存活。
在JDK1.2之后,Java对对象的引用概念进行扩充, 将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom
Reference)
强引用就是指在程序代码之中普遍存在的、类似于“Object obj = new Object()”这类的引用。只要强引用还存在,垃圾回收器就永远不会回收被引用的对象。
2、垃圾回收算法
①标记-清除算法
最基础的收集算法是“标记-清除”算法:分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
主要不足:一是效率问题,标记和清除两个过程的效率都不高,二是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致在程序运行过程中,需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
②复制算法
将可用内存划分为两部分,当一部分用完时,将这部分中所有还存活的对象复制到另一部分中,再将已使用过的内存空间一次清理掉。
③标记-整理算法
与标记-清除算法类似,但标记后的后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动。
④分代收集算法
当前商业虚拟机的垃圾回收都采用“分代收集”算法。这种算法并没有什么新的思想,只是根据对象存活周期的不同,将内存划分为新生代、老年代。根据不同年代的特点,采用不同的回收算法。
3、垃圾回收器
①Serial收集器:
②G1收集器
对象优先在Eden分配:
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代:所谓大对象是指需要大量连续内存空间的Java对象,最典型的的大对象就是很长的字符串以及数组。