深入理解Java虚拟机_JVM高级特性与最佳实践(第3版)学习笔记

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()方法可以主动让出执行时间,但是如果想要主动获取执行时间,线程本身是没有什么办法的

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值