栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

理论修炼---JVM之内存结构

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

理论修炼---JVM之内存结构


文章目录
  • 前言
  • 一、程序计数器(PC寄存器)
    • 程序计数器的作用
      • 程序计数器的特点
  • 二、Java虚拟机栈
    • 定义
      • 压栈和出栈过程
      • 局部变量表
      • 操作数栈
      • 方法的调用
        • 静态链接
        • 动态链接
        • 方法绑定
      • Java 虚拟机栈的特点
    • 三、本地方法栈
    • 四、堆
      • 堆的特点
      • 新生代于老年代
      • 对象的分配过程
      • Minor GC、Major GC、Full GC的区别
        • 定义
        • 触发机制
      • 逃逸分析
      • TLAB(Thread local allocation buffer)
      • 四种引用方式
    • 方法区
      • 方法区定义
      • 方法区特点
      • 运行时常量池
  • 总结


前言

文章为温习Java虚拟机所作笔记。

Java虚拟机的内存空间分为五部分:

  1. 程序计数器
  2. Java虚拟机栈
  3. 本地方法栈
  4. 堆
  5. 方法区


一、程序计数器(PC寄存器)

  程序计数器是一块较小的内存空间,是存储当前线程所执行的字节码指令地址,如果线程执行的是本地方法(C/C++方法)那么程序计数器为未定义

程序计数器的作用
  1. 能够让字节码解释器改变程序计数器来依次读取指令,也就是代码的流程控制。
  2. 在多线程的情况下,程序计数器就是记录当前线程执行的位置(可以理解为执行到哪个方法了),假设当前线程为A,那么当从线程B切换回A时,现在A就可以通过程序计数器知道上次执行到哪里了。
程序计数器的特点
  1. 是一块较小的内存空间;
  2. 生命周期:随着线程创建而创建,随着线程销毁而销毁;
  3. 线程私有,每个线程都有一个程序计数器;
  4. 唯一一个不会出现OutOfMemoryError错误的内存空间。
二、Java虚拟机栈

Java虚拟机栈是描述Java方法运行过程的内存模型

定义

会每一个即将运行的方法创建一个栈帧区域,用于存放方法运行过程中的一些信息,如:

  1. 局部变量表;
  2. 操作数栈;
  3. 动态链接;
  4. 方法出口信息等。
压栈和出栈过程
  1. 在方法执行过着需要创建局部变量时,将局部变量存入栈中局部变量表中。
  2. Java虚拟机栈的栈顶的栈帧是正在执行的活动栈帧,也就是当前执行的方法,PC寄存器也会指向这个地址,只有活动栈帧的本地变量可以被操作数栈使用,如果活动栈帧中调用另一个方法,那么与之对于的栈帧会被创建并被压入栈中(栈顶),成为新的活动栈。
  3. 方法结束这个栈被移出,这个栈帧的返回值会变成新活动栈中操作数栈的一个操作数,如果栈帧没有返回值则,新的活动栈中操作数栈的操作数没有变化
局部变量表

  局部变量表被定义为一个数组,主要存储方法的参数,对象的引用,定义在方法体内部的局部变量,各类基本数据类型,和返回地址类型
局部变量表的大小是在编译器确定下来的,基本储存单元是slot,32位占用一个,62位占用两个,

  1. Java虚拟机栈会为每个slot分配一个索引,通过所以可以访问到局部变量表中指定的局部变量值;
  2. 如果栈帧是一个实例方法或构造方法,那么局部变量表中索引为0的slot会存放这个对象的this地址,其他参数按顺序存放
  3. 局部变量表是可以复用的,如果一个局部变量过了作用域,那么新的局部变量会用这个slot,目的就是节省资源
  4. 在栈帧中与性能调优最密切相关的就是局部变量表,局部变量是重要的垃圾回收根节点,只要被局部变量直接或间接引用的对象都不会被回收
操作数栈

操作数组是存储在内存中的,频繁存储读写会影响性能,他会有一个明确的深度在编译器就会被确定,详细了解可以自行百度

方法的调用

主要由以下三个点组成:

静态链接

  字节码文件被加载进jvm内部的时候,如果被调用的方法在编译器可以知道,并且被调用方法在运行的时候不变,就可以将调用方的符号引用改为直接引用的过程称为静态链接,因为他是不变的

动态链接

  如上,字节码文件被加载进jvm内部的时候,被调用的方法没办法在编译器知道,只能在运行时将调用方的符号引用改为直接引用的过程叫动态链接,因为运行时才知道这个过程是动态的

方法绑定
  • 早期绑定
    被调用目标方法在编译器可知,且运行不变。静态链接属于这类
  • 晚期绑定
    被调用的目标方法在编译器不可知,只能在运行时根据实际类型绑定相关方法,动态链接属于这类
Java 虚拟机栈的特点
  • 运行速度非常快仅次于PC寄存器
  • 局部变量表随着栈帧的创建而创建,在编译器确定大小,创建时只需要分配在编译器确定的大小即可,在方法运行过程中局部变量表的大小不会改变
  • Java虚拟机栈有两种异常:StackOverFlowError 和 OutOfMemoryError。
    StackOverFlowError :若Java虚拟机大小不允许动态拓展,则当前线程请求的深度超过当前Java虚拟机栈的最大深度时,抛出这个异常,不停递归或调用方法超过Java虚拟机栈规定的深度会抛出该异常。
    OutOfMemoryError :若运行动态拓展,当前线程请求栈内存用完了,无法在动态拓展就会抛出OutOfMemoryError 异常。
     不允许动态拓展时超过Java虚拟机栈最大深度就会抛出StackOverFlowError 异常,允许动态拓展但当前线程分配的栈内存用完了会抛出OutOfMemoryError 异常。
  • Java虚拟机栈是线程私有的,随着线程创建而创建,线程结束而销毁。
  • 出现StackOverFlowError 异常时,内存可能还有很多,因为只是超过虚拟机栈规定深度而不一定是内存不足。

常见的运行时异常有:

  • NullPointerException - 空指针引用异常
  • ClassCastException - 类型强制转换异
  • IllegalArgumentException - 传递非法参数异常
  • ArithmeticException - 算术运算异常
  • ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
  • IndexOutOfBoundsException - 下标越界异常
  • NegativeArraySizeException - 创建一个大小为负数的数组错误异常
  • NumberFormatException - 数字格式异常
  • SecurityException - 安全异常
  • UnsupportedOperationException - 不支持的操作异常
三、本地方法栈

 本地方法栈是JVM为执行本地方法准备的空间,由于很多Native方法都是由C语言实现的,通常又叫C栈,它于Java虚拟机栈实现的功能类似,只不过是描述本地方法运行过程的内存模型。
 本地方法执行,也会在本地方法栈创建一块栈帧,用于存放该方法的局部变量、操作数栈、动态链接、静态链接、方法出口信息等,方法执行结束后相应的方法也会出栈,并释放内存空间,也会抛出StackOverFlowError 和 OutOfMemoryError异常。

四、堆

堆是用来存放对象的,几乎所有的对象都存在堆中。

堆的特点
  • 线程共享的,整个Java虚拟机只有一个堆,所有线程都访问同一个堆,而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个。
  • 堆在虚拟机启动时创建,是垃圾回收的主要场所。
  • 堆可分为新生代和老年代
  • Java虚拟机也规定,堆可以处于物理上不连续的内存空间,但逻辑上应该当作连续的。

Java堆使用的内存不需要保证连续性,但是由于堆是被线程共享的,所以访问的时候需要注意同步的问题,方法和属性都要保持一致性。

新生代于老年代
  • 老年代比新生代生命周期要长
  • 新生代与老年代默认空间比例是1:2,新生代占1、老年代占2,新生代占整个堆的1/3。
  • 新生代中Eden 空间和另外两个 Survivor 空间缺省所占的比例是:8:1:1。
  • 几乎所有的 Java 对象都是在 Eden 区被 new 出来的,Eden 放不了的大对象,就直接进入老年代了。
对象的分配过程

这部分描述的不够详细,可以自行百度查看更详尽的回收流程

  • 创建的对象先放在eden区,大小有限制。
  • 创建新对象时Eden区满了会触发Minor GC,将Eden区不在被其他对象引用的对象进行销毁,在加载新- 的对象到Eden区,这里要注意的是Survivor区满了不会触发Minor GC,而是Eden空间满了,Minor Gc才顺便清理Survivor0区。
  • 将上述Eden 中剩余的对象移到 Survivor0 区
  • 再次触发垃圾回收,上次Survivor下来的,在Survivor0 区,如果没有回收,会被复制到Survivor1区
  • 再次经历垃圾回收,又回将幸存者放到Survivor0 区,以此类推
Minor GC、Major GC、Full GC的区别 定义

Minor GC:清理年轻代空间(包括Eden和survivor)释放在Eden区域中不活跃的对象,若释放后Eden空间还是放不下新对象就会将活跃的对象放入survivor区,survivor作为Eden和老年代的中间交换区域,当老年代空间足够时,在survivor区被循环超过15次的对象将会放入老年代中。survivor1向survivor2复制时对象大小大于survivor2的可用内存,则直接把该对象转入老年代中。

Majro GC:清理老年代空间,当老年代空间不足时,JVM会在老年代进行Major GC。

Full GC: 清理整个堆空间包括年轻代和老年代。

触发机制

Minor GC:年轻代满时会触发,指的是Eden区满会触发,survivor满不会触发。

Full GC:

  • 老年代空间不足
  • 方法区空间不足
  • 通过minor GC 进入老年代的平均大小>老年代的可用内存会触发
逃逸分析

逃逸分析(从JDK1.7开始默认开启)

Java中的对象都是在堆中分配吗?说明为什么!

逃逸分析就是 分析Java对象的动态作用域
 一个对象被定义之后,如果能被外部对象引用,就称之为方法逃逸,如果被其他线程引用称之为线程逃逸

例如下面这段代码,s是一个内部变量,直接将s返回这个StringBuffer 对象可能在其他地方被修改导致作用域就不单单是在方法内部 ,就算是一个局部变量,但是逃逸到了方法外,这就是所谓的方法逃逸

public static StringBuffer createStringBuffer(String s1, String s2) {

    StringBuffer s = new StringBuffer();

    s.append(s1);

    s.append(s2);

    return s;
}

修改后:

public static StringBuffer createStringBuffer(String s1, String s2) {

    StringBuffer s = new StringBuffer();

    s.append(s1);

    s.append(s2);

    return s.toString;
}

使用逃逸分析,编译器可以对代码做如下优化:

  • 同步省略:如果一个对象被发现只能从一个线程访问,对于这个对象的操作可以不考虑同步问题。
  • 将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

在编译期间,如果经过JIT逃逸分析,发现对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。

TLAB(Thread local allocation buffer)

中文意思是:线程本地分配缓存区

  • 是属于Eden区的,是线程专用的内存分配区域,线程私有,默认开启(不是绝对,看虚拟机类型)占用Eden区域的1%,大对象无法分配给TLAB。
  • 堆是全局共享的,同一时间,可能会有很多线程在堆上申请空间,这种情况下要加锁处理,效率自然降下来了
  • 所以使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用直接的TLAB,可以使线程同步,提高分配效率。
  • 并不是所有的对象都可以在TLAB分配内存成功。
  • TLAB中内存不足时,按照比例值处理,当TLAB小于比例值时,放弃当前TLAB重建一个来分配,默认比例是1/64,收到分配请求后TLAB如果没有足够的空间,就判断是否小于1/64,则重新创建TLAB。
  • 如果重新创建的TLAB空间还是放不下这个对象,则直接去Eden创建。
四种引用方式
  • 强引用:创建一个对象,赋值给一个引用变量,普通new出来的对象都是强引用,有引用变量指向时永远不会被垃圾回收,即使抛出oom,解决办法是将引用赋值为null,这个时候指向的对象就会被垃圾回收
  • 软引用:如果一个对象具有软引用,内存空间足够不会回收,内存空间不够就会回收,只有没有被回收就可以被使用。
  • 弱引用:非必须对象,jvm垃圾回收时,无论内存是否充足都会回收弱引用关联的对象
  • 虚引用:并不会觉得对象的生命周期,如果一个对象持有虚引用,跟没有引用一样,随时可能会被回收
方法区 方法区定义

Java虚拟机规范中定义方法区是堆的一个逻辑部分

方法区存放以下信息:

  • 已经被虚拟机加载的类信息
  • 常量
  • 静态变量
  • 即时编译后的代码
方法区特点
  • 线程共享:方法区是堆的一个逻辑部分,因此和堆一样被线程共享。整个虚拟机只有一个方法区
  • 永久代:方法区的信息一般是长期存在,又是堆的逻辑分区,在堆中被划分为永久代
  • 内存回收效率低,在方法区中信息一般是长期存在,回收一遍后可能只有少量无效信息,主要回收目标是对常量和类型的卸载。
  • Java虚拟机对方法区的规定比较松,和堆一样运行固定大小和动态拓展,也允许不实现垃圾回收
运行时常量池

 方法区中存放:类信息,常量,静态变量,即时编译后的代码,常量存放在常量池中。当类被java虚拟机加载后.class文件中的常量就存放在方法区的运行时常量池中,在运行时可以向常量池添加新的常量,String 类的 intern() 方法就可以向常量池添加字符串常量

总结

 到这里文章已经结束了,主要结合其他文章说明了自身对java虚拟机的五个部分的理解。

参考文章如下:

参考:Java中的逃逸分析,通俗易理解
参考:【JVM】 TLAB到底是干什么的
参考:JVM 底层原理最全知识总结
参考:Minor GC、Major GC和Full GC之间的区别

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/993141.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号