Java内存区域
运行时数据区
程序计数器
虚拟机栈
Java虚拟机栈是线程私有的,它的生命周期与线程相同
虚拟机栈描述的是Java方法执行的线程内存模型
局部变量表所需的内存空间在编译期间完成分配
在虚拟机栈区规定了两类异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果无请到足够的内存,将抛出OutOfMemoryError异常
本地方法栈
作用与虚拟机栈非常相似,区别只是本地方法栈为本地方法服务,同样规定了StackOverflowError、OutOfMemoryError两类异常
堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
堆的唯一目的就是存放对象实例
通过参数-Xmx和-Xms设定堆的大小。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常
方法区
Java方法区是被所有线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
方法区的内存回收目标主要是针对常量池的回收和对类型的卸载
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError异常
运行时常量池
运行时常量池是方法区的一部分
常量池表用于存放编译期生成的各种字面量和符号引用,在类加载后存放到方法区的运行时常量池中
运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据
本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,抛出OutOfMemoryError异常
HotSpot虚拟机对象探秘
对象的内存布局
- 对象头:对象自身的运行时数据(Mark Word)、类型指针(确定该对象是哪个类的实例)
- 实例数据
- 对齐填充
对象的访问定位
- 句柄:reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference本身不需要被修改
- 直接指针:速度更快,它节省了一次指针定位的时间开销
垃圾收集器与内存分配策略
对象存活算法
- 引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的
很难解决对象之间的相互循环引用
- 可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的
引用
JDK1.2版本后对引用的概念进行了扩充,强度由强到弱依次为
- 强引用:程序代码中普遍存在的引用赋值
- 软引用:发生oom前,回收掉只被软引用关联的对象
- 弱引用:下一次垃圾收集发生时,回收掉只被弱引用关联的对象
- 虚引用:为一个对象设置虚引用关联的唯一目的,只是为了能在这个对象被收集器回收时收到一个系统通知
两次标记
- 第一次标记:对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法
- 第二次标记:对F-Queue中的对象进行小规模的标记
回收方法区
- 回收常量:常量池中的常量、类、方法、字段的符号引用
- 回收类型:不再被使用的类
垃圾收集算法
分代收集理论
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
- 强分代假说(Strong Generational Hypothesis)
熬过越多次垃圾收集过程的对象就越难以消亡
把Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域。在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
- 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数
标记-清除算法
标记-复制算法
标记-整理算法
内存分配原则
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
基础故障处理工具
- jps:虚拟机进程状况工具
- jstat:虚拟机统计信息监视工具
- jinfo:Java配置信息工具
- jmap:Java内存映像工具,获取堆转储快照
- jhat:虚拟机堆转储快照分析工具
虚拟机类加载机制
类加载的时机
双亲委派模型
Java内存模型与线程
Java内存模型
《Java虚拟机规范》中曾试图定义一种“Java内存模型” (Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到 内存和从内存中取出变量值这样的底层细节
- Java内存模型规定了所有的变量都存储在主内存中(类比于物理硬件的主内存)
- 每条线程还有自己的工作内存(类比于处理器的高速缓存)
- 线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据
- 线程间变量值的传递均需要通过主内存来完成
内存模型的定义:8种原子操作
volatile变量
- 内存可见性
基于volatile变量的运算在并发下并不线程安全,因为Java里的操作运算符并非原子操作
- 禁止指令重排序
happens-before原则
- 程序次序规则
- 管程锁定规则
- volatile变量规则
- 线程启动规则(start)
- 线程终止规则(join、isAlive)
- 线程中断规则(interrupt)
- 对象终结规则(finalize)
- 传递性
线程
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和 执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度
线程的实现
内核线程
使用内核线程实现的方式也被称为1:1实现。内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调 度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个 内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1 的关系称为一对一的线程模型
在主流平台的主流商用Java虚拟机普遍采用1:1的线程模型,即每一个Java线程都是直接映射到一个操作系统原生线程来实现的
局限性:
-
由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换
-
每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈 空间),因此一个系统支持轻量级进程的数量是有限的
用户线程
使用用户线程(User Thread,UT)实现的方式被称为1:N实现。用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型
局限性:
没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的
混合实现
将内核线程与用户线程一 起使用的实现方式,被称为N:M实现
线程调度
- 协同式 (Cooperative Threads-Scheduling)线程调度
实现简单。线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。因为切换操作对线程自己是可知的,所以一般没有什么线程同步的问题
线程执行时间不可控制。如果一直不告知系统进行线程切换,那么程序就会一直阻塞在 那里
- 抢占式(Preemptive Threads-Scheduling)线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
Java使用的线程调度方式就是抢占式调度。在Java中,有Thread::yield()方法可以主动让出执行时间,但是如果想要主动获取执行时间,线程本身是没有什么办法的