都说搞C的牛叉,那是因为C解决问题,全靠程序员自己,他们对自己的程序在内存中是什么样了如指掌。而Java呢不需要有太多操作系统的知识,不用时刻注意内存的问题,但这不代表我们就不用去了解它背后的原理,(其实我是被公司的C嘲讽了一下OoO)。Java之所以容易上手,那是因为最困难的问题,已经被前人解决了,而这一切都归功于Java Virtual Machine-Java虚拟机,JVM其实就是一个抽象的计算机,它有自己的指令集,有自己的机器语言,有自己的内存管理。本系列会一一解开它的真面目。
本文基于Java HotSpot™ 虚拟机,JDK 1.8,将讨论:
JVM 内部结构
JVM 内存管理
JVM 内存模型
图 1 JVM 内部结构
1. JVM 内部结构
一个程序执行的过程是这样的,以C语言为例,源代码首先被编译成可执行文件,以二进制的形式存放到磁盘上,当执行时,首先从磁盘加载到内存中,然后处理器就开始执行目标程序中机器指令。反观Java,首先编译成字节码文件,与平台无关,JVM通过ClassLoader加载到内存中,然后执行其中的机器指令,JVM帮我们跟操作系统打交道。有了字节码和JVM,Java实现了平台无关性。JVM也可以认为是一个进程,在启动时申请一块内存,然后按照功能的不同,把内存分为以下不同区域:
(1) Heap
堆,一个非常重要的区域,被所有线程共享,基本所有的对象实例都在这里分配,大部分的垃圾回收也发生在这里。这部分内存,由JVM使用Garbage Collector(自动内存管理工具)来管理,职责就是为对象分配内存,释放空闲内存。Java 堆的大小可以使用参数来控制是固定的,还是动态扩展的。
(2) JVM Stacks
栈,与线程息息相关,线程私有,随线程生而生,死而死,是线程执行工作的内存。HotSpot中Java栈和本地方法栈合二为一,都在本地内存空间中分配,这部分内存就不需要JVM刻意的去管理了。JVM栈主要用来存储栈帧,当调用一个方法时创建一个栈帧,方法结束销毁,从栈的角度来看,就是入栈和出栈两个操作。
栈帧是一个数据结构,用来存储局部变量,操作数栈,和当前类运行时常量池的引用。局部变量数组:用来保存方法内定义的基本类型变量,下标从 0 开始,JVM使用局部变量表传递方法参数,当调用一个实例方法,第 0 位置存储的是当前对象的 this 引用;操作数栈:用来执行运算和准备调用方法的参数以及方法的返回结果;动态链接:引用对象的运行时常量池。
(3) PC Register
程序计数器,线程私有,主要作用就是存储指令地址,取指,解码和执行。每个线程都关联着唯一的栈和PC寄存器。
(4) Metaspace
元空间,在JDK8之前的HotSpot VM称之为方法区或者永久代,被各线程共享,它存储了一个类的结构信息,如常量池、字段、方法等。存放在本地内存中,与堆不相关。
(5) Native Method Stacks
JVM栈是为Java方法准备的,那么本地方法栈则是为虚拟机调用本地方法服务的。
2. JVM 内存管理
Java不允许直接操作内存,内存的申请和释放统一交给虚拟机处理。
2.1 Garbage Collection 自动内存管理
自动内存管理(以下简称GC)的职责:
分配内存
确保引用对象保留在内存
回收不可达引用对象的内存
GC 解决了大部分的内存分配问题,它本身也占用一定的资源。当堆满了或者是它的某个组成部分达到一个阀值就会触发垃圾回收。垃圾回收主要从这几方面考虑:回收的频率和时间,比如堆小那么回收的次数就多,堆大回收次数少,但回收一次的时间长;内存碎片问题;多线程程序下的垃圾回收。
垃圾回收策略:
(1)串行与并行(Serial versus Parallel)
串行,同一时间只能有一个垃圾回收线程工作;并行,在多CPU系统中,可有多个垃圾回收线程同时工作。
(2)并发与Stop-the-world(Concurrent versus Stop-the-world)
Stop-the-world 垃圾回收器,在回收期间,应用程序完全暂停工作,此时堆就相当于被冻结了,对象的状态不可变;并发,一个或多个垃圾收集任务与应用程序同时执行,可能会出现短暂的Stop-the-world,在收集时,对象的状态可能会改变。
(3)复制(Copying)
将内存分为两半,回收时将存活的对象复制到另一半空间,然后清除当前内存,后续内存的分配比较容易,但内存的利用率比较低。
(4) 标记清除与标记整理(Compacting versus Non-compacting)
标记清除,标记可回收的对象,统一进行回收,不进行内存压缩,会产生大量的内存碎片,在分配大对象时,可能无法找到连续的内存;标记整理,在标记完后,首先对内存进行压缩整理,把所有存活的对象放到一起进行回收。
(5)分代收集
把堆分成几个区域,新生代和老年代,不同的区域使用上面的不同的回收方法。
2.2 HotSpot 中的分代收集
在 HotSpot 中,把内存分为新生代和老年代,新生代又分为Eden和两个大小一样的Survivor空间,大部分对象在Eden分配,一些大对象可能直接分配到老年代。
堆的结构如下:
图 2 堆