目录
OutOfMemoryError异常和StackOverflowError异常
详解 JVM 内存模型
JVM内存模型主要分为:运行时数据区、执行引擎、本地库接口、本地方法库
JVM 运行时的数据区包括 5 个部分,如下图所示。
Heap(堆)和方法区是所有线程共享的,栈、本地方法栈和程序技术器是各个线程独占的。
线程独占的:
栈也叫方法栈或者Java虚拟机栈,是线程私有的,线程在执行每个方法时都会同时创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。
局部变量表用于存放:各种基础数据类型、对象引用。
操作数栈用于存放:方法内部各个操作的具体操作数,这些操作数可以是任意的Java数据类型。
动态连接:指向方法的直接引用。
栈帧: 是用来存储数据和部分过程结果的数据结构。
栈可能出现的两种异常:
(1)jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。如:递归调用没有结束点。
(2)虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。如:不断创建活跃的线程。最终会抛出:OutOfMemoryError: unable to create new native thread。
本地方法栈与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行 Java 方法使用栈,而执行 native 方法使用本地方法栈。
程序计数器保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空。作用:cpu单核在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了在线程切换可以恢复到正确执行位置。
线程共享的:
堆是JVM 管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例和数组,几乎所有的对象实例都在这里分配。
当堆内存没有可用的空间时,会抛出 OOM (OutOfMemoryError)异常,即:OutOfMemoryError:Java heap space。
根据对象存活的周期不同,JVM 把堆内存进行分代管理,分为新生代和老年代。新生代又可以分为Eden空间、From Survivor空间、To Survivor空间,由垃圾回收器来进行对象的回收管理。通过设置 -Xmx和-Xms可以配置堆的最大和最小内存空间。
方法区(也叫非堆区)也是各个线程共享的内存区域,又叫非堆区。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,JDK 1.7 中的永久代和 JDK 1.8 中的 Metaspace 都是方法区的一种实现。当方法区没有可用空间时,会抛出OOM(OutOfMemoryError)异常,即:OutOfMemoryError: PermGen space。可以通过 -XX:PermSize和-XX:MaxPermSize限制方法区大小(1.8 是 -XX:MetaspaceSize、-XX:MaxMetaspaceSize)。
JVM虚拟机之外的内存:直接内存
直接内存:其实就是机器内存,比如NIO在使用的时候,传输数据就可以利用直接内存传输。
不会在堆中分配的对象
栈上分配、标量替换会导致对象不会在堆内存中分配。
逃逸分析就是指分析对象动态作用域,当一个对象在方法中被定义后,可能被外部方法引用,比如作为调用参数传递到其他方法,成为方法逃逸。甚至还有可能被外部线程访问到,比如赋值给类变量并且可以在其他线程中访问的实例变量,称为线程逃逸。
栈上分配:如果确定一个对象不会逃逸出方法之外,那么这个对象就会创建在栈中,对象占用的内存空间随栈帧出栈而销毁。
标量替换:标量是指一个数据不能分解成更小的数据了,Java中原始数据类型以及对象和数组的引用都可以称为标量。相对的,如果一个数据可以继续分解,那它就称为聚合量,Java对象就是典型的聚合量,如果根据程序的访问情况,将其使用到的成员变量恢复原始类型来访问就叫标量替换。如果逃逸分析证明一个对象不存在方法逃逸和线程逃逸,并且这个对象可以被拆解成标量,那么程序执行的时候可能不用在堆上创建该对象,而是在栈上创建被这个方法使用到的该对象拆分后的成员变量。
HotSpot虚拟机对象
对象创建
对象创建的两种方式:指针碰撞、空闲列表
指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点指示器,那分配内存就仅仅是把指针想空闲的那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。
空闲列表:假设Java堆中内存不是规整的,用过的内存和空闲的内存相互交错,这个时候就没法使用指针碰撞分配内存了,虚拟机就必须维护一个空闲列表,这个列表记录了哪些内存是空闲可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并且更新空闲列表,这种分配方式就称为空闲列表。
在使用基于复制算法的垃圾收集器时,系统采用的分配算法就是指针碰撞。如Serial、ParNew、Parallel Scavenge
而使用基于标记-清除算法的垃圾收集器时,系统采用的分配算法就是空闲列表。如CMS
对象在内存中的布局
在HotSpot虚拟机中,对象在内存中的存储布局分为3个部分:对象头、实例数据、对象填充
对象头包含两部分信息:Mark Work和类型指针。
Mark Work 用于存放对象自身的运行数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
而锁状态标志、线程持有的锁、偏向线程ID就是synchronized关键字实现偏向锁、轻量级锁的关键。
对象访问定位
对象访问定位主要有两种方式:一种是使用句柄,另一种是使用直接指针。
使用句柄的话,Java堆中会有一块内存来作为句柄池,栈中存放的引用就是存放的对象句柄地址,句柄才是真正的指向具体的实例对象数据和方法区中的对象类型数据。
使用直接指针,Java堆中对象实例数据和对象类型指针放在同块内存,栈中存放的引用就是实例数据的地址,对象类型指针指向方法区中的对象类型数据。
OutOfMemoryError异常和StackOverflowError异常
- 设置启动参数 -XX:+HeapDumpOnOutOfMemoryError,可以让虚拟机在内存溢出时Dump出当前的内存堆转储快照
堆内存溢出会打印:java.lang.OutOfMemoryError:Java heap space - StackOverflowError一般是因为单个线程中栈帧太大或者虚拟机栈容量太小,导致的栈空间不足而抛出异常。
- 方法区空间不足也会引起OutOfMemoryError,
Java内存模型JMM
Java内存模型其实就是在特定的操作协议下,对特定的内存和高速缓存的抽象描述。也可以简单的理解为Java内存模型中工作内存就是特定的Cpu高速缓存的抽象,主内存就是硬件层面特定内存的抽象。
在JVM内部使用的java内存模型(JMM)将线程堆栈和堆之间的内存分开。JMM 是 Java 内存模型,与 JVM 内存模型是两回事,JMM 的主要目标是定义程序中变量的访问规则,如下图所示,所有的共享变量都存储在主内存中共享。每个线程有自己的工作内存,工作内存中保存的是主内存中变量的副本,线程对变量的读写等操作必须在自己的工作内存中进行,而不能直接读写主内存中的变量。
在多线程进行数据交互时,例如线程1 给一个共享变量赋值后,由线程2 来读取这个值,线程1 修改完变量是修改在自己的工作区内存中,线程2 是不可见的,只有从 线程1 的工作内存写回主内存,线程2 再从主内存读取自己的工作工作内存才能进行进一步的操作。由于指令重排序的存在,这个写—读的顺序有可能被打乱。因此 JMM 需要提供原子性、可见性、有序性的保证。
JMM 如何保证原子性、可见性,有序性
原子性
JMM 保证对除 long 和 double 外(long和double的非原子协定)的基础数据类型的读写操作是原子性的。隐式锁 synchronized 也可以提供原子性保证。synchronized 的原子性是通过 Java 的两个高级的字节码指令 monitorenter 和 monitorexit 来保证的。显式锁Lock也可以保证原子性。Atomic使用CAS来保证原子性。
可见性
JMM 可见性的保证,一个是通过 synchronized,另外一个就是 volatile(看这里)。volatile 强制变量的赋值会同步刷新回主内存,强制变量的读取会从主内存重新加载,保证不同的线程总是能够看到该变量的最新值。
有序性
对有序性的保证,主要通过 volatile 和一系列 happens-before 原则。volatile 的另一个作用就是阻止指令重排序,这样就可以保证变量读写的有序性。
happens-before 原则包括一系列规则,如:
-
程序顺序原则,即一个线程内必须保证语义串行性;
-
锁规则,即对同一个锁的解锁一定发生在再次加锁之前;
-
happens-before 原则的传递性、线程启动、中断、终止规则等。