跳转至

深入理解JVM-1

本页统计信息
  • 本页约 1689 个字, 23 行代码, 预计阅读时间 6 分钟。

  • 本页总阅读量

Part1:Java内存管理

1.1 Java的运行时数据区

  • Java虚拟机在执行Java程序的时候会把它管理的内存划分为若干个不同的数据区域,这些区域有各自的创建和销毁的时间,而Java的运行时数据区域包含这样几个部分

  • 方法区:

    • 存储已经被虚拟机加载的类的信息、常量、静态变量和JIT编译器编译后的代码,JVM的规范把方法区描述成了堆的一个逻辑部分
    • 这个区域中垃圾回收方法比较少见
    • 运行时常量池 Runtime Constant Pool
    • Java的class文件中除了有类的版本、字段、方法、接口等信息描述之外,还需要有常量池用于存放编译器生成的字面量和符号引用,这一部分内容将在类加载到方法区之后,在常量池中存放
    • 动态性:常量不一定需要编译器才能产生,运行期间也可以将新的常量放入常量池中,这种特性用的比较多的就是String类中的intern( )方法
  • 堆:

    • 是JVM管理的内存中最大的一块,是线程共享的的一块内存区域,在虚拟机启动的时候就会创建
    • 所有对象的实例和数组都需要在堆上分配内存,也是垃圾收集器的主要管理区域
    • 线程共享的Java堆可以划分出多个线程私有的分配缓冲区 TLAB
    • Java堆的内存空间不一定是物理上连续的,只要是逻辑上连续就可以
    • 下面这段代码是非常常见的堆溢出异常,引发原因是对象实例过多
    public class HeapOOM {
        static class OOMObject {
            int a;
        }
        public static void main(String[] args) {
            List<OOMObject> list = new ArrayList<OOMObject>();
            while (true) {
                list.add(new OOMObject());
            }
        }
    }
    
  • 虚拟机栈:

    • Java的虚拟机栈也是线程私有的,生命周期和所属线程的生命周期相同
    • 描述了Java方法执行的内存模型,在每个方法执行的时候会创建一个栈帧(Stack Frame),用于存储
    • 局部变量表
      • 存放了编译期就可以知道的数据类型(基本的boolean,byte,char,int等八种)、对象引用和returnAddress类型
      • 其中long和double需要占用2个局部变量空间slot(槽),其他的都是1个空间
      • 局部变量表在运行期间就完成了分配,一个方法的分配的局部变量空间是完全确定的,因此方法运行期间不会改变局部变量的大小
    • 操作数栈
    • 动态链接
    • 方法出口
    • 每一个方法从调用到执行结束的过程,就是一个栈帧在VMStack中入栈到出栈的过程
    • 这个区域会抛出StackOverFlowError异常和OutOfMemoryError异常
  • 本地方法栈:

    • 和虚拟机栈发挥的作用比较类似,区别是本地方法栈执行的是本地的方法
  • 程序计数器:

    • 比较小的一块内存空间,和汇编语言中的PC一样用来指示当前运行的字节码的行号,各种分支、循环、跳转和异常处理都依赖PC实现
    • Java虚拟机的多线程是通过线程的切换和分配处理器执行时间的方式来实现的,在任何一个确定的时候,一个处理器只能处理一个线程,因此每个线程都需要一个单独的PC,因此是线程私有的
    • 如果执行的是本地的方法,则此时PC的值是undefined
  • 不同数据区域的特性

    • 这些区域中,方法区和堆直接和执行引擎进行交互,是线程共享的。
    • 而虚拟机栈、本地方法栈和程序计数器直接合本地库接口进行交互,是线程之间独立的
  • 直接内存 Direct Memory

  • 不是JVM中定义的内存区域,但是也被频繁使用
  • 这是JDK后期引入的NIO类,基于通道和缓冲区的I/O方式,可以使用Native函数库直接分配堆外的内存,可以使用本地函数库直接分配对外内存,然后通过DirectByteBuffer对这些区域进行引用的需要

1.2 垃圾回收机制 GC

  • 常见的回收判断算法

  • 要回收首先需要使用算法来判断对象是否可以回收

  • 引用计数算法:记录每个对象被引用的次数,如果引用的次数为0就说明需要回收内存

    • 但事实上Java用的并不是这种策略,可以用下面一段代码来说明
    public class refGC {
        public Object instance = null;
        private static final int _1MB = 1024;
        private byte[] bigMem = new byte[2 * _1MB];
        public static void main() {
            refGC a = new refGC(), b = new refGC();
            a.instance = b;
            b.instance = a;
            a = null;
            b = null;
        }
    }
    
    • 这段代码中虽然AB互相引用了,但是事实上还是作为垃圾被回收了,说明Java使用的并不是引用计数的策略
  • 可达性分析算法:Java中使用的方法

    • 用一个对象作为GC-Roots,每次垃圾回收层root开始向下搜索,搜索的路径称为引用链
    • 如果一个对象和root之间没有任何引用链,那么就需要进行回收
    • Java语言中可以作为root的对象包括
    • 虚拟机栈中应用的对象
    • 方法区类静态属性引用的对象
    • 方法区常量引用的对象
    • 本地方法栈中本地方法引用的对象
  • 对象被回收的时候,可以用finalize方法进行“自救”

  • 引用的多种类型

  • 强引用,只要强引用还存在,就不能把对象作为垃圾来回收,new出来的就是强引用

  • 软引用:用来描述一些还有用但是并非必须的对象,软引用的关联的对象在系统将要发生内存溢出的异常之前,会对它们进行二次回收,之后还不行再抛出OOM异常
  • 弱引用:描述非必须的对象,引用的强度比软引用还要若,垃圾回收的时候一定会回收掉只有弱引用的对象
  • 虚引用:最弱的引用,基本没啥用

1.2.2 垃圾回收算法

  • 标记-清除算法:
  • 先标记出有哪些垃圾需要回收,再进行回收
  • 问题是效率太低了,并且会在回收之后产生大量的不连续内存
  • 复制算法:
  • 将内存分成两块,一次只使用其中一块,需要垃圾回收的时候就把不需要回收的内存直接复制到另一块过去,然后把那一块内存清除,如此循环往复
  • 实现简单,运行高效
  • 标记-整理算法:
  • 标记之后,回收的时候直接向前移动,这样就避免了一半内存的浪费

颜色主题调整

评论区~

有用的话请给我个star或者follow我的账号!! 非常感谢!!
快来跟我聊天~