Java虚拟机栈是线程私有的,它的生命周期与线程相同
虚拟机栈描述的是Java方法执行的线程内存模型
局部变量表所需的内存空间在编译期间完成分配
在虚拟机栈区规定了两类异常:
作用与虚拟机栈非常相似,区别只是本地方法栈为本地方法服务,同样规定了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异常
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的
很难解决对象之间的相互循环引用
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的
JDK1.2版本后对引用的概念进行了扩充,强度由强到弱依次为
熬过越多次垃圾收集过程的对象就越难以消亡
把Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域。在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
类加载的时机
双亲委派模型
《Java虚拟机规范》中曾试图定义一种“Java内存模型” (Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到 内存和从内存中取出变量值这样的底层细节
内存模型的定义:8种原子操作
基于volatile变量的运算在并发下并不线程安全,因为Java里的操作运算符并非原子操作
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和 执行调度分开,各个线程既可以共享进程资源(内存地址、文件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实现
实现简单。线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。因为切换操作对线程自己是可知的,所以一般没有什么线程同步的问题
线程执行时间不可控制。如果一直不告知系统进行线程切换,那么程序就会一直阻塞在 那里
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
Java使用的线程调度方式就是抢占式调度。在Java中,有Thread::yield()方法可以主动让出执行时间,但是如果想要主动获取执行时间,线程本身是没有什么办法的