Jvm学习之内存结构

我想知道更多

  • 内存模型:并发编程相关
  • 内存结构:JVM相关

运行时的数据区域

Jvm学习之内存结构_第1张图片

Java虚拟机在运行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域,有的区域随着虚拟机进程的启动而存在,有的区域随着用户线程的启动和结束而建立和销毁

程序计数器
  • 字节码解释器需要改变该计数器的值去选取下一条执行的字节码指令,如分支、循环、异常处理和线程恢复等基础功能
  • 线程私有,各线程之间互不影响。每个线程都需要独立的程序计数器
  • 如果是native方法,则计数器值为空(native 方法是Java程序调用了非Java代码,是一种引入其它语言程序的接口);如果正在执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址
  • 程序计数器也是在Java虚拟机规范中唯一没有规定任何OutOfMemoryError异常情况的区域
Java虚拟机栈
  • 线程私有,声明周期与线程相同,每个方法执行(不是创建)时都会创建一个栈帧(栈帧和栈不是一回事,栈帧只是栈的一段区域,有栈顶和栈底)。栈帧中存有局部变量表,操作数栈,动态链接,方法出口等信息
    • 局部变量表:存放了编译期可知的各种基本数据类型(boolean, byte, char, short, int, long, float, double),对象引用和returnAddress(指向了一条字节码的地址)。其中,long和double占两个栈位。局部变量表所需的内存空间在编译期完成分配,运行期不会改变
    • 操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
    • 动态连接:每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接
    • 方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
  • 该区域可能出现两种异常情况:StackOverflowError(线程请求的栈的深度大于虚拟机允许的)和OutOfMemoryError(虚拟机栈扩展时无法申请到足够的内存)
本地方法栈
  • 与Java虚拟机栈发挥的作用十分相似,可抛出的异常一样
  • 不同之处在于虚拟机栈为执行字节码服务,而本地方法栈为执行本地方法中的语言(如C)服务
  • 有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一
Java 堆
  • 被所有线程共享,虚拟机启动时创建。唯一作用存放实例对象,几乎所有对象都在此分配内存

  • 是垃圾收集器管理的主要区域,所以Java堆也叫GC堆

    • 从内存回收的角度来说,采用分代收集策略

      1. 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1:1(关闭自动变化的参数是-XX:-UseAdaptiveSizePolicy,关掉之后是严格的8:1:1)
      2. 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象

      Jvm学习之内存结构_第2张图片

    • 从内存分配的角度来说哦,可能会再次划分出多个线程私有的分配缓冲区。这是为了防止多次请求分配空间的并发导致的线程不安全

  • 可以处在物理不连续的区域,逻辑连续即可。

  • Java堆既可以是固定大小,也可以是可扩展的(通过-Xmx和-Xms实现),无法扩展时,抛出OutOfMemoryError

方法区
  • 所有线程共享,存储被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等
  • 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。对于HotSpot来说,方法区也被称作持久代
  • 该区域主要目标是针对常量池的回收和对类型的回收
  • 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收
  • 当方法区无法满足内存分配时,将抛出OutOfMemoryError
运行时常量池
  • 运行时常量池也是方法区的一部分,虚拟机加载Class后把常量池中的数据以及翻译出的直接引用放入运行时常量池
  • Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放的编译期间形成的各种字面量和符号引用,在编译期形成
    • 字面量:文本字符串、声明为final的常量值等
    • 符号引用::类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
  • Java虚拟机对Class文件的每个部分格式都有严格规定,但对于运行时常量池,Java虚拟机规范没有任何细节要求
  • 运行时常量池相对于常量池的另外一个特征就是具备动态性,Java语言并不要求常量一定只有编译期才能产生,运行期间也可将新的常量放入池中
  • 当常量池无法申请内存时会抛出OutOfMemoryError

PS

  • JDK1.6之前字符串常量池位于方法区之中

  • JDK1.7字符串常量池已经被挪到堆之中

  • 举个栗子

    public class Test1 {
        public static void main(String[] args) {
    
            //String 的 intern 方法首先将尝试在常量池中查找该对象,如果找到则直接返回该对象在常量池中的地址;找不到则将该对象放入常量池后再返回其地址
    		 String s1 = new StringBuilder("星星").append("stalean").toString();
    		 System.out.println(s1.intern() == s1);
    
    		 String s2 = new StringBuilder("星星").append("stalean").toString();
    		 System.out.println(s2.intern() == s2);
    		//1.7新加测试
             System.out.println(s2.intern() == s1);
    	}
    
    }
    

    对于jdk1.6来说,结果为false,false ,因为字符串常量池在方法区中,而s1和s2则指向java堆中

    对于jdk1.7来说,结果为true,false,true ,因为此时常量池在java堆中。当s1申请完空间后,指向java堆中的常量池,所以s1==s1.intern()为true,当s2初始化时,它指向了一个实例,而s2.intern()指向的还是之前的常量池中的对象,所以为false

直接内存
  • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

HotSpot对象

对象的创建
  1. 当虚拟机遇到一条new指令后

    • 会先到常量池中看能否定位到该类的符号引用,并且检查该符号引用的类是否被加载、解析和初始化过
    • 如果没有,则执行响应的加载过程
  2. 类加载完成后,虚拟机将对新生对象分配内存,此时对象所需内存大小已经确定

    分配方式可分为

    • 指针碰撞:适用于内存绝对规整
    • 空闲列表:当内存不规整时,需要维护一个列表

    解决并发下的线程安全问题

    • 对分配内存空间的动作进行同步处理,采用CAS+失败重试的方式保证原子性
    • 使用TLAB,即本地线程分配缓冲
  3. 分配完成后,内存空间初始化为零值(不包括对象头)并为对象头设置所需要的信息。如果使用TLAB方式,则该步骤在其之前

  4. new以后,执行init方法,对象创建完成

对象的内存布局
对象头
  • MarkWord:存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁等
  • 类型指针,指向方法区中所对应的类(在Hotspot中符合),如果是通过句柄的话,就没有类型指针
  • 如果是数组的话,对象头中还应有记录数组长度的数据
实例数据
  • 对象真正存储的有效信息,即代码中所定义的各种类型的字段内容,包括在基类中定义的
  • 存储顺序受到JVM分配策略以及字段在Java源码中定义顺序的印象
  • HotSpot默认是longs/doubles_ints_shorts/chars_bytes/booleans_oops
对齐填充
  • 不是必然存在的。HotSpot要求对象大小必须是8字节整数倍,需要填充
对象的访问定位
  • 使用句柄

    [外链图片转存失败(img-mTJsFSsS-1565842038554)(E:\Images\java\句柄访问.PNG)]

    • 优点:当GC移动对象实例时,只需要修改句柄中的实例数据指针就行了
  • 使用直接指针(HotSpot)

    [外链图片转存失败(img-Q7jsE2UY-1565842038555)(E:\Images\java\指针访问.PNG)]

    • 优点:节省了一次指针定位的开销

参考

  • 《深入理解Java虚拟机》

  • Java虚拟机的内存组成以及堆内存介绍-HollisChuang’s Blog

  • Java堆和栈看这篇就够 - Johnny-Zhuang’s Technology Blog

  • Java虚拟机的堆、栈、堆栈如何去理解? - 知乎

  • Java 内存之方法区和运行时常量池 - 漠然的博客 | mritd Blog

  • 可以通过该网站来获取head分析工具

附:

这是我之前写的一个程序,可能对理解有帮助

package reportjava;

/**
 * 实验报告虚方法多态
 * @author stalean
 * @date 2019年6月23日15:07:28
 */
class A {
    //产生新的虚(virtual)方法MethodVirtual(),new slot
    void  MethodVirtual() {
        System.out.println("aV");
    }
    //产生新的虚(virtual)方法MethodVirtual1,new slot
    void  MethodVirtual1(){
        System.out.println("aV1");
    }

}
class B extends A {
    // 覆盖父类的MethodVirtual()方法,reuse slot
    @Override
    void  MethodVirtual() {
        System.out.println("bV");
    }
    // 覆盖父类的MethodVirtual1()方法,reuse slot
    @Override
    void  MethodVirtual1()	{
        System.out.println("bV1");
    }
}
class C extends B {

/*
	void  MethodVirtual() {
			System.out.println("cV");
	}

	void  MethodVirtual1() {
			System.out.println("cV1");
	}
*/

}
class D extends C {
    // 覆盖MethodVirtual()方法
    @Override
    void MethodVirtual() {
        System.out.println("dV");
    }

    // 覆盖MethodVirtual()1方法
    @Override
    void  MethodVirtual1() {
        System.out.println("dV1");
    }
}
public class A_Polymorphism {
    public static void main(String[] args) {

        A a;
        B b;
        C c;
        D d;

        a = new A();
        b = new B();
        c = new C();
        d = new D();

        A ab = b;
        A ac = c;
        A ad = d;

        B bc = c;
        B bd = d;

        C cd = d;


        System.out.println("--------------------方法多态---------------------------");

        System.out.println("--------------------a.MethodVirtual()---------------------------");
        a.MethodVirtual();
        ab.MethodVirtual();
        ac.MethodVirtual();
        ad.MethodVirtual();

        System.out.println("--------------------a.MethodVirtual1()---------------------------");
        a.MethodVirtual1();
        ab.MethodVirtual1();
        ac.MethodVirtual1();
        ad.MethodVirtual1();

        System.out.println("--------------------b.MethodVirtual()---------------------------");
        b.MethodVirtual();
        bc.MethodVirtual();
        bd.MethodVirtual();

        System.out.println("--------------------b.MethodVirtual1()---------------------------");
        b.MethodVirtual1();
        bc.MethodVirtual1();
        bd.MethodVirtual1();

        System.out.println("--------------------c.MethodVirtual()---------------------------");
        c.MethodVirtual();
        cd.MethodVirtual();

        System.out.println("--------------------c.MethodVirtual1()---------------------------");
        c.MethodVirtual1();
        cd.MethodVirtual1();

        System.out.println("--------------------d.MethodVirtual()---------------------------");
        d.MethodVirtual();

        System.out.println("--------------------d.MethodVirtual1()---------------------------");
        //d = null;
        d.MethodVirtual1();
    }
}

Jvm学习之内存结构_第3张图片

你可能感兴趣的:(Java成神之路)