# 运行时数据区域

Java虚拟机运行时数据区

# 程序计数器(线程私有)

  • 程序计数器:当前线程所执行的字节码的行号指示器(分支、 循环、 跳转、 异常处理、 线程恢复等基础功能)。
  • 唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

# Java 虚拟机栈(线程私有)

  • 每个方法在被执行时虚拟机都会创建一个栈帧(Stack Frame)用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。
  • 局部变量表存放了编译期可知的各种基本数据类型(boolean、 byte、 char、 short、 int、float、 long、 double)、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。
  • 局部变量表所需的内存空间在编译期间完成分配。
  • 方法运行期间不会改变局部变量表大小。

虚拟机栈异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
  • 虚拟机栈在动态扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。(hotSpot 虚拟机栈内存不可动态扩展)

# 本地方法栈(线程私有)

  • 本地方法栈为虚拟机使用到的 Native 方法服务。(Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

# Java 堆(线程共享)

  • 是 Java 虚拟机所管理的最大、被所有线程共享的一块内存区域,在虚拟机启动时创建。
  • 是垃圾收集器管理的主要区域,又称 “GC 堆”(Garbage Collected Heap)。
  • 从内存回收的角度来看(采用分代收集算法)新生代和老年代或(Eden 空间、 From Survivor 空间、 To Survivor 空间等)。
  • 从内存分配的角度 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
  • Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
  • 虚拟机通过 - Xmx 和 - Xms 控制堆内存扩展,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

# 方法区(线程共享)

  • 用于存储已被虚拟机加载的类型信息、 常量、 静态变量、 即时编译器编译后的代码等数据。(又称:Non-Heap(非堆))
  • JDK8 以前 HotSpot 虚拟机使用永久代(Permanent Generation)来实现方法区,1.8 之后使用本地内存中实现的元空间(Meta-space)代替永久代。

# 运行时常量池

  • (Runtime Constant Pool)是方法区的一部分。
  • Class 文件中除了有类的版本、 字段、 方法、 接口等描述信息外,还有一项信息是常量池。
  • 在类加载时(加载 class 文件)会将编译期生成的各种字面量和符号引用以及翻译出来的直接引用进入方法区的运行时常量池中存放。
  • 运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性。例如 String 类的 intern()方法

# 直接内存

  • NIO 使用 channel/buffer 的 I/O 方式,使用 Native 函数库直接分配堆外内存,通过堆中 DirectByteBuffer 作为该部分内存引用能够显著提高性能。
  • 本机直接内存的分配不会受到 Java 堆大小的限制,但仍会收到本机总内存(包括物理内存、 SWAP 分区或者分页文件) 大小以及处理器寻址空间的限制。

# HotSpot 虚拟机对象探秘

# 对象的创建

  • 类型检查与类加载:(虚拟机)类加载检查(当遇到 new 指令时,根据指令参数在常量池中寻找这个类的符号引用并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。如果没有就必须先执行相应的类加载过程)。
  • 分配内存:(虚拟机)为新生对象分配内存(对象所需内存的大小在类加载完成后便可完全确定)。常用两种方法:指针碰撞、空闲列表并发情况下为保证线程安全通常采用 TLAB 方式分配内存,虚拟机是否使用 TLAB,可以通过 - XX:+/-UseTLAB 参数来设定。
  • 初始化零值:(虚拟机)将分配到的内存空间都初始化为零值。
  • 设置对象信息(对象头):(虚拟机)设置对象头(类的元数据信息、 对象的哈希码、 对象的 GC 分代年龄等信息)。
  • 初始化对象(构造器):(java 程序)执行 <init> 方法(由字节码中是否跟随 invokespecial 指令所决定),初始化对象(例如加载构造方法)。

# 对象的内存布局

# 对象头(Header)

  • 自身的运行时数据(Mark Word):如哈希码(HashCode)、 GC 分代年龄、 锁状态标志、 线程持有的锁、 偏向线程 ID、 偏向时间戳等
    HotSpot虚拟机对象头 Mark Word
  • 类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

# 实例数据(Instance Data)

  • 对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
  • HotSpot 虚拟机默认的分配策略为 longs/doubles、 ints、 shorts/chars、bytes/booleans、 oops(Ordinary Object Pointers)

# 对齐填充(Padding)

  • 起着占位符的作用:由于对象的大小必须是 8 字节的整数倍。 而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

# 对象的访问定位

# 句柄访问

  • Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
    通过句柄访问对象
  • reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。

# 直接指针访问

  • reference 中存储的直接就是对象地址
    通过直接指针访问对象
  • 速度更快,它节省了一次指针定位的时间开销。(Sun HotSpot 虚拟机就采用这种方式访问对象)

# Java 堆溢出

# Java 堆内存溢出异常测试

  • 测试代码
/**
*VM Args:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError
*@author zzm
*/
public class HeapOOM{
static class OOMObject{
}
    public static void main(String[] args){
    List<OOMObject>list=new ArrayList<OOMObject>();
    while(true){
        list.add(new OOMObject());
}}}
  • 运行结果
java.lang.OutOfMemoryError:Java heap space
Dumping heap to java_pid3404.hprof……
Heap dump file created[22045981 bytes in 0.663 secs]

# 虚拟机栈和本地方法栈溢出

  • 测试代码(单线程)
/**
    *VM Args:-Xss128k
    *@author zzm
    */
    public class JavaVMStackSOF{
    private int stackLength=1;
    public void stackLeak(){
    stackLength++;
    stackLeak();
    }
    public static void main(String[]args)throws Throwable{
        JavaVMStackSOF oom=new JavaVMStackSOF();
        try{
            oom.stackLeak();
        }catch(Throwable e){
        System.out.println("stack length:"+oom.stackLength);
    throw e;
    }}}
  • 运行结果
stack length:2402
Exception in thread"main"java.lang.StackOverflowError
at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:20)
at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
……后续异常堆栈信息省略
  • 测试代码(多线程)
/**
*VM Args:-Xss2M(这时候不妨设置大些)
*@author zzm
*/
public class JavaVMStackOOM{
private void dontStop(){
while(true){
}}public void stackLeakByThread(){
while(true){
Thread thread=new Thread(new Runnable()){
@Override
public void run(){
dontStop();
}};
thread.start();
}}
public static void main(String[]args)throws Throwable{
JavaVMStackOOM oom=new JavaVMStackOOM();
oom.stackLeakByThread();
}}
  • 运行结果
Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread

# 方法区和运行时常量池溢出

  • 测试代码
/**
*VM Args:-XX:PermSize=10M-XX:MaxPermSize=10M
*@author zzm
*/
public class RuntimeConstantPoolOOM{
public static void main(String[]args){
// 使用 List 保持着常量池引用,避免 Full GC 回收常量池行为
List<String>list=new ArrayList<String>();
//10MB 的 PermSize 在 integer 范围内足够产生 OOM 了
int i=0;
while(true){
list.add(String.valueOf(i++).intern());
}}}
  • 运行结果
Exception in thread"main"java.lang.OutOfMemoryError:PermGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

HotSpot 提供元空间的防御措施

  • -XX: MaxMetaspaceSize: 设置元空间最大值, 默认是 - 1, 即不限制, 或者说只受限于本地内存大小。
  • -XX: MetaspaceSize:指定元空间的初始空间大小(字节),达到该值就会触发垃圾收集进行类型卸载。同时收集器根据释放掉的空间调整该值大小。
  • -XX: MinMetaspaceFreeRatio:在垃圾收集之后控制最小的元空间剩余容量的百分比
  • -XX: Max-MetaspaceFreeRatio:控制最大的元空间剩余容量的百分比。

# String.intern()

public class RuntimeConstantPoolOOM{
public static void main(String[]args){
String str1=new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern()==str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
}}
  • String.intern()是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。
  • jdk1.7 之后将字符串常量池从方法区转移到堆内存。