第一章概述
Java程序员把内存控制的权利交给了JVM
第二章:运行时数据区域
Java虚拟机执行Java程序的过程会把它所管理的内存分配为若干个数据区域。这些数据区域有给各自的用途:有的随着JVM虚拟机进程启动二而一直存在(堆和方法区),有的区域随着用户线程的启动和结束进行建立和销毁(虚拟机栈、程序计数器和本地方法栈)
概念模型代表了所有虚拟机的一个统一模型概述,但是各款Java虚拟机可能实现不同,可能会通过一种更为高效的方式去实现它。
一:程序计数器
程序计数器是一块内存很小的区域,是当前线程所执行字节码的行号指示器。
字节码计时器的工作就是通过改变程序计数器当中的值来取下下一个需要执行的字节码命令。
JVM多线程是通过线程切换分配处理器的执行时间来实现的。一个处理器内核在同一时刻只能处理一个线程,线程切换回来之后可以到达正确的执行位置考得就是程序计数器存储的下一个字节码指令的位置。
线程中执行的是Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果是native方法的话,程序计数器中的值是空。
二:Java虚拟机栈
虚拟机栈线程私有,生命周期与线程相同。
虚拟机栈秒描述的是Java方法执行的内存模型,每个方法被执行的时候虚拟机栈都会同步创建一个方法的栈帧,栈帧用于存储局部变量表,动态链接,操作数栈,方法出口等信息。
每一个方法被调用的过程就对应一个栈帧的入栈和出栈。栈的最重要的特点就是后进先出,先进后出。
局部变量表当中存放的是编译器可知的把中基本类型和引用类型类型。这里引用的不是对象本身而是对象的起始地址的引用指针或者是句柄或者是地址相关的信息。对于HotSpot虚拟机来讲肯定就是对象的实际地址了。
这些内容在局部变量表中的存储单位是变量槽。Double和Long类型占用两个,其余都是一个。编译器之后局部变量表大小(变量槽数量)已定,运行时不会改变。32位虚拟机和64位虚拟机变量槽大小是不一致的。
线程创建时申请栈空间失败的话也会发生OutOfMemoryError的。
线程请求栈深度大于虚拟机允许深度会发生StackOverflowError异常。
如果栈深度允许动态拓展的话,栈深度在动态拓展的时候如果申请不到足够的内存的话就会发生OutOfMemoryError异常。
关于动态拓展这种情况,HotSpot虚拟机不支持动态拓展。这种虚拟机只会在线程创建的时候无法获取足够的栈空间导致发生OutOfMemoryError
三:Java堆
Java堆内存是JVM管理内存最大的一块。所有线程共享,虚拟机启动时创建,唯一的目的就是为了存放对象实例,所以这里是垃圾回收的主战场。
几乎所有的对象实例都在这里分配,至少当前还是。十年前,绝对主流的HotSpot虚拟机内部垃圾收集器都是基于经典分带来设计,新生代和老年代垃圾收集器配合工作。随着垃圾收集器的发展,HotSpot当中出现了不基于经典分带的垃圾收集器。
备注:新生代包括一个伊甸园区和两个幸存者区,默认比例大小是8:1:1,除了一个新生代之后,还包括一个老年代。历史上多个垃圾回收期使用这种设计:HotSpot,Self,Smalltalk
分配缓冲区(TLAB)这玩意肯定是在新生代当中,线程私有的分配缓冲区是为了提升对象分配的效率。Java堆进行细分只是为了更快的分配内存和更好的垃圾回收。
Java堆可以处于物理上不连续的内存区域,但是逻辑上应该看做连续的。对于大对象,比如数组,多数虚拟机实现简单粗暴,可能会要求空间连续。
Java堆大小可以是固定的,也可以动态扩展。动态拓展的参数可以通过-Xmx 和 -Xms来进行设定。堆内存无法分配实例,并且无法动态扩展的时候会发生OutOfMemoryError
四:方法区
方法区是各个线程共享的区域存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存。
JDK8以前HotSpot虚拟机设计团队把垃圾收集器的分带设计拓展到方法区,HotSpot虚拟机能够管理Java堆一样,管理这个这部分内存不用专门编写这部分内存管理代码。
方法区也不需要连续的内存,可以固定大小可以动态拓展,甚至可以不进行垃圾回收。
这个区域的内存回收目标是常量池的回收和内存的卸载。
方法区无法分配新的内存的时候,就会发生OutOfMemoryError异常。
五:运行时常量池
运行时常量池时方法区的一部分
Class文件中有类的版本、字段、方法、接口信息外,还有常量池表用于存放编译器生成的各种字面量和符合引用,这部分内容类加载之后会放入到运行时常量池当中。除了Class文件中描述的符号引用之外,还会把由符号引用翻译出来的直接引用也放入到方法区的运行时常量池当中。
运行时常量池相对于Class文件的常量池还具有动态性。Java中常量编译器和运行时都会产生。运行时产生的常量放入到常量池中的方法用的最多的是String类的intern()方法。
运行时常量池是方法区的一部分,此处无法申请或者说是分配到内存的时候就会发生OutOfMemoryError异常
六:直接内存
直接内存不是JVM运行时数据区的一部分。这里也可能发生OutOfMemoryError异常
NIO的使用可以使用Native函数库直接分配堆外内存,然后通过存储在堆里面的一个DirectByteBuffer这样的对象引用直接内存中的这块区域。这样一些场景可以显著提高性能,避免了数据在堆内存和直接内存当中的来回复制。
直接内存收到物理机总内存和寻址空间的限制。我们在配置JVM参数是经常忽略这块内存的限制,导致动态扩展是发生OutOfMemoryError异常。