JVM——内存分配

《深入理解Java虚拟机》出第三版了,工作一年多的时候看过第二版,重新复习一遍JVM

一、内存分配

JVM管理的内存包括5个运行时数据区域。

  • 线程私有:程序计数器,虚拟机栈,本地方法栈
  • 线程共享:堆,方法区

1、程序计数器PC

①程序计数器的作用

JVM中程序计数器是一块很小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。

即Java文件编译成class文件后,会被编译成很多字节码指令,例如iadd,istore等,

程序计数器是用来记录当前线程正在执行的字节码指令的地址,如果执行的是本地方法,计数器为Undefined

它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等都是需要依靠PC来完成的

PS : CPU中也有一个PC寄存器,作用是保存下一条CPU指令的地址,默认是每次执行指令后+1。

②程序计数器的由来

JVM的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的(《操作系统原理》中进程调度策略:时间片轮转法),

单核处理器在一个时间片内,仅能处理一个线程中的字节指令,时间片结束后,进入下一个时间片,单核处理器会处理另一个线程的字节码指令,

为了线程切换后能恢复到正确的位置,每条线程都需要有一个独立的程序计数器,独立存储--即程序计数器是线程私有的内存。

③内存溢出

程序计数器是唯一一个没有规定任何OutOfMemoryError情况的内存区域。

2、Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程相同。

① Java虚拟机栈的作用

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完毕的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程

② 局部变量表

局部变量表:存放编译期可知的各种Java虚拟机

  • 基本数据类型(boolean,byte,char,short,int,float,long,double)、
  • 对象引用类型(reference:指向对象起始地址的指针或者句柄)
  • returnAddress(指向一条字节码指令的地址),

局部变量表是以槽(Slot)为单位表示的,Slot大小为32bit,PS:long与double大小是64bit,需要两个Slot存储,不具备原子性,不是线程安全的。

局部变量表所需的内存空间在编译期间完成分配的,即编译期已经固定了内存大小,运行期间不会改变局部变量表的大小。

//很有意思的问题,下面两种写法中,
//在编译期就已经确定了栈帧中局部变量表的大小
public void test(){
    for(int i = 0; i < 10000; i++){
        //这里不会创建10000个reference引用,每次超出作用域范围,旧的obj都会被覆盖
        //所以跟下面写法,区别不是很大,不会引起栈溢出
        //优点可能就是,尽可能缩小引用的作用域范围
        Object obj = new Object();
    }
}

public void test(){
    Object obj;
    for(int i = 0; i < 10000; i++){
        //这里引用的作用与范围,比上面要广
        //但是由于没有引用所以,object对象GC可能会快一点。
        obj = new Object();
    }
}

③内存溢出

虚拟机栈区域会出现两种异常状况

栈溢出:如果线程请求的栈深度大于虚拟机所允许的站深度,将抛出StackOverflowError异常

内存溢出:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

常见的HotSpot虚拟机栈是不可以动态扩展,所以不会由于扩展而导致的OOM异常,即只要线程申请栈空间成功之后就不会有OOM异常,申请栈失败还是会发生OOM异常的

3、本地方法栈

本地方法栈返回的作用与虚拟机栈的作用非常类似,线程私有的,并且本地方法栈为仅为虚拟机用到的本地方法服务。

与虚拟机栈相同,本地方法栈也会出现栈溢出与内存溢出。

常见的HotSpot虚拟机将本地方法栈和虚拟机栈合二为一了

4、堆

①堆的作用

Java Heap是JVM中所管理内存中最大的一块,是线程共享的。

Java Heap是在JVM启动时创建的,它的唯一作用就是存放对象实例。《Java虚拟机规范》中“所有的对象实例以及数组都应当在堆上分配”。

Java Heap是垃圾收集器管理的内存区域,所以也称为“GC堆”,GC后面学习

②TLAB

TLAB全称Thread Local Allocation Buffer,从分配内存的角度看,Java堆每个线程都会划分出当前线程私有的分配缓存区TLAB。

TLAB的作用就是保证A线程创建Object,B线程创建Object在堆中是内存隔离的。不会产生A创建一个Object对象,B也创建一个Object对象,两个Object对象是同一个情况。

PS:Java Heap的区域划分(老年代,新生代,TLAB)的目的:为了更好的回收内存,或者更快的分配内存。

③内存溢出

Java堆可以被实现为固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展实现的(通过-Xmx和-Xms设定)。

如果Java堆中没有内存完成实例分配,并且堆也无法再扩展,Java虚拟机将会抛出OutOfMemoryError异常。

5、方法区

 方法区跟Java堆一样,是线程共享的。

①方法区的作用

方法区用于存储被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。

②关于永久代

永久代与方法区并不等价,由于HotSpot虚拟机设计团队选择吧收集器的分代设计扩展到方法区,或者说使用永久代来实现方法区而已,使得HotSpot的GC能够像管理Java堆一样管理这块内存区域,省去了为方法区编写内存管理代码的逻辑。其他虚拟机(如JRockit、IBM J9)实现并不存在永久代的概念。

当然HotSpot虚拟机的这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有上限:-XX:MaxPermSize,不设置也会存在默认值,可能远小于本地内存)

因此,

JDK6HotSpot放弃了永久代,逐步改用本地内存来实现方法区

JDK7HotSpot已经把原本放在永久代的字符串常量池,静态变量等移出Java堆中

JDK8HotSpot完全废弃了永久代的概念,改用和JRockit、IBM J9一样用本地内存来实现的元空间Metaspace来代替,把JDK7中剩余的永久代内容(主要是类型信息)全部移到元空间中

以上版本的更替意味着:JDK8以后的HotSpot虚拟机的方法区一部分在Java堆中(字符串常量池,静态变量),一部分在元空间Metaspace中(类型信息)

 

③运行时常量池

运行时常量池时方法区的一部分。Class文件中除了类的版本信、字段、方法、接口等描述信息外,还有一项信息是常量池表,用来存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

运行时常量池时方法区的一部分,会受到方法区大小的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常

6、直接内存

 直接内存不是虚拟机运行时数据区的一部分,但是这部分内存页被频繁的使用,而且也可能导致OutOfMemoryError异常出现。

JDK1.4加入了NIO实现,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作(避免还要从堆外内存IOcopy到Java堆中)。

直接内存是堆外内存,Java堆内存+直接内存<物理内存,但通过-Xmx参数设置Java堆内存时,若忽略直接内存的考虑,Java堆内存<物理内存,可能出现Java堆内存+直接内存>物理内存,导致OOM异常。

posted on 2020-04-21 19:20  FFStayF  阅读(223)  评论(0编辑  收藏  举报