学习《深入理解Java虚拟机》总结

程序计数器:

线程私有的内存,每个线程都有一个独立的程序计数器互不影响。用来记录下一条要执行的字节码指令,线程切换后恢复到正确的执行位置,分支、循环、跳转、异常处理等均需依赖程序计数器。是唯一不会发生内存溢出的区域。

虚拟机栈:

线程私有的内存,生命周期与线程相同。描述的是java方法执行的线程内存模型,每个方法被执行时虚拟机都会创建一个栈帧,用来存储局部变量表、操作数栈、动态连接、方法出口等信息。

局部变量表--存放方法参数、方法内部定义的局部变量(编译器可知的基本数据类型、对象引用、returnAddress类型)。

异常--线程请求深度大于虚拟机允许的深度,抛出StrackOverflowError异常;如虚拟机栈支持动态扩展,无法扩展时抛出内存溢出异常。

本地方法栈:

线程私有的内存,为虚拟机使用本地(Native)方法提供服务,程序计数器为空。

异常--线程请求深度大于虚拟机允许的深度,抛出StrackOverflowError异常;如虚拟机栈支持动态扩展,无法扩展时抛出内存溢出异常。

Java堆:

线程共享的内存(物理上不连续),用来存放对象实例(非绝对,逃逸分析、栈上分配等优化)及数组,可以固定大小也可动态扩展(取决于-Xmx,-Xms参数)。

异常--如果堆中没有内存完成实例分配,且堆也无法继续扩展时,抛出内存溢出异常。

方法区:

线程共享的内存,存储虚拟机加载的类型信息(类名、访问修饰符、字段描述、方法描述)、常量、静态变量、即时编译器编译后的代码缓存等数据。

运行时常量池:

方法区的一部分,存放编译期生成的字面量与符号引用,运行时也可将新的常量放入池中(String.intern())。

异常--内存溢出异常。

堆上分配内存的方式:

  1. 指针碰撞:内存规整、采取的垃圾收集器带有空间压缩整理能力。
  2. 空闲列表:非规整内存,由虚拟机维护一个列表,记录哪些内存块可用,分配时找到一块足够的空间分配。

多线程如何保证并发安全:CAS或本地缓冲区分配(每个线程在Java堆中预先分配一块区域,分配时需同步)。

对象的内存布局:

对象头:哈希码、GC分发年龄、锁状态、偏向线程ID、类型元数据指针

实例数据:代码中定义的各种类型的字段内容,含父类中定义的

对其填充:占位符补齐,8字节的整数倍

对象的访问定位:

栈上的reference访问堆上的具体对象。

  1. 句柄访问:堆中分配一块内存作为句柄池,reference存储对象的句柄地址,包含到对象实例(堆)的指针、到对象类型数据(方法区)的指针。
  2. 直接指针(hotspot):reference直接存储对象的地址,包含对象实例数据、到对象类型数据(方法区)的指针。

优劣分析:句柄访问存储的是地址,在对象移动(GC)时只改变句柄中的实例数据指针,reference本身不用修改。指针访问仅访问对象本身时速度更快。

如何判断对象可回收:

  1. 引用计数算法:在对象中添加一个引用计数器,没有一个地方引用+1,引用失效-1,为0时表示对象可回收。

问题:无法解决循环依赖问题。

  1. 可达性分析:如果对象到GCRoots间没有任何引用链相连,表示对象可回收。

注:对象真正回收经理两次标记,上述方式为第一次,如对象覆盖了finalize()方法(不建议)且未执行过,将执行finalize()尝试一次自救。

如何在并发环境下进行可达性分析:

三色标记:

白色-对象尚未被垃圾收集器访问,分析结束后仍为白色代表不可达。

黑色-对象已被垃圾收集器访问,且对象的所有引用均已扫描。

灰色-对象已被垃圾收集器访问,但至少还有一个引用没被访问。

解决方案:

  1. 增量更新:黑色对象插入新的指向白色对象的引用关系,重新以黑色对象为根扫描一次,类似将黑色变为灰色。--CMS收集器
  2. 原始快照:灰色对象删除指向白色对象的引用关系,重新以灰色对象为根进行扫描。--G1收集器

如何确定GCRoots:(全局引用-常量、类静态属性、执行上下文-栈帧中的本地变量表)

  1. 虚拟机栈中引用的对象,如正在运行方法所用到的参数、局部变量、临时变量等。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中引用的对象(JNI、Native方法)。
  5. 虚拟机内部的引用,如基本数据类型对应的Class对象、常驻的异常对象、系统类加载器。
  6. 被同步锁持有的对象。
  7. 反应虚拟机内部情况的JMXBean、JVMTI中注册的回调等。

注:不用扫描所有的进行GCRoots枚举,hotspot内使用了一组OopMap的数据结构记录,类加载完成时,计算出对象什么偏移量上时什么类型的数据,收集器在扫描时可以直接获取。

同时为了避免引用关系变化,设置了“安全点”,当所有线程都到达设定的安全点时开始收集。--一条汇编指令轮询是否在安全点中断(poll)。

对于sleep、blocked状态的线程,设置安全区,安全区确保在一段代码片段中引用关系不会发生变化。

引用:

  1. 强引用:如Object obj = new Object();强引用关系存在垃圾收集器就不会回收被引用的对象。
  2. 软引用:通过SoftReference类来实现软引用,描述还有用但非必须的对象,发生内存溢出前会堆这些对象尝试二次回收。
  3. 弱引用:通过WorkReference类来实现弱引用,描述非必须的对象,仅能存活到下次垃圾收集。
  4. 虚引用:通过PhantomReference类来实现虚引用,虚引用不会对生存事件构成影响,也无法通过虚引用获得一个对象实例,唯一作用能在这个对象被垃圾收集器回收时收到一个通知。

垃圾收集算法:

分代收集理论:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集的对象越难以消亡。
  3. 跨代应用假说:跨代引用相对于同代引用仅占极少数。

--为解决新生代引用老年代对象,又避免扫描整个老年代,可在新生代建立一个全局的记忆集,将老年代划分为若干个小块,发生Minor GC时仅将引用的小块内存里的对象加入GCRoots进行扫描。

标记-清除算法:

步骤:标记所有可被回收的对象后,统一回收被标记对象。(反之亦可:标记不可回收)。

缺点:1、效率低。

    2、内存碎片化,标记、清除后会产生大量不连续的内存碎片,如需要分配较大对象时,可能触发垃圾收集。

标记-复制算法:

步骤:分为一块较大的Eden空间,两块较小的Survivor空间(默认比例8:1:1),每次分配内存只使用Eden和一块Survivor,垃圾收集后将存活对象放置另一块Survivor区。

缺点:1、资源稍有浪费,具体取决与分区比例。

  2、需要依赖其他区域进行分类担保,如Survivor区无法装下收集后的存活对象,将会移至担保区(一般为老年代)。

标记-整理算法:

步骤:标记所有存活对象,将存活对象统一向内存空间的一端移动。

缺点:1、对象移动需要暂停用户线程(STW)。

各类收集器分析:

衡量指标:内存占用、吞吐量、延迟

新生代:

Serial:

单线程垃圾收集器,新生代采取“标记-复制”算法,整个GC阶段暂停用户所有线程,所有收集器里额外内存消耗最少的,hotspot虚拟机客户端模式下的默认新生代收集器。

适用范围:运行在客户端模式下的虚拟机。

ParNew:

Serial收集器的多线程并行版本,新生代采取“标记-复制”算法(多线程并行),整个GC阶段暂停用户所有线程。性能并不一定比Serial收集器佳。

Parallel Scavenge:

基于“标记-复制”算法实现的多线程并行处理器,关注于吞吐量(处理用户代码时间与处理器运行总时间),具有自适应调节策略(动态分配新生代、老年代大小)。

适用范围:在后台运算而不需要太多交互的分析任务。

老年代:

Serial Old:

单线程垃圾收集器,老年代采取“标记-整理”算法,整个GC阶段暂停用户所有线程。

适用范围:运行在客户端模式下的虚拟机,JDK5以前与Parallel Scavenge搭配使用,或用作CMS收集器失败时的后备方案。

Parallel Old:

基于“标记-整理”算法实现的多线程并行处理器,关注吞吐量,具有自适应调节策略。

使用范围:注重吞吐量或者处理器资源较为稀缺的场合。

CMS:

是一款以获取最短回收停顿时间为目标的收集器,基于标记-清除算法,当内存空间碎片化影响对象分配时,采用标记-整理算法收集一次。基于增量更新做并发标记。

步骤:初始标记(STW,仅标记GC Roots直接关联的对象,单线程)->并发标记->重新标记(STW)->并发清除

缺点:1、并发阶段占用线程资源,导致应用程序变慢。

  2、无法处理“浮动垃圾”--并发标记、并发清理阶段与用户线程并发进行,会产生新的垃圾对象,老年代需要预留空间,空间无法分配时,启用Serial Old收集。

  3、基于“标记-清除”算法,会产生大量空间碎片。

适用范围:关注服务的响应速度,系统停顿时间尽可能短,带来更好的交互体验。

Garbage First(G1):

基于Region的内存布局,提供并发类卸载支持,按区域优先级进行回收,在有限的时间里获取尽可能高的收集效率。

步骤:初始标记(STW,仅标记GC Roots直接关联的对象)->并发标记->最终标记(STW,原始快照方式)->筛选回收(对各个Region的回收价值和成本进行排序,根据设置的期望停顿时间制定回收计划,将需要回收Region内的存活对象转移到新的Region内,清空旧的Region,因为涉及对象移动需要STW)。

与CMS的比较:

优:CMS整体基于“标记-清除”算法,会产生空间碎片,G1采取“标记-整理”算法,两个Region间采取“标记-复制”算法。

劣:G1为垃圾收集产生的内存占用和程序运行的额外负载高,约占整个堆空间的20%。

综合:CMS在小内存上性能更优,G1在大内存能发挥优势,平衡点在8GB左右,但后续G1的官方优化空间更大。

使用范围:在延迟可控的情况下获得尽可能高的吞吐量。

类加载机制:

阶段:

  1. 加载
    • 通过一个类的全限定类名获取此类的二进制流
    • 将字节流代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
  2. 验证
  • 文件格式验证:魔数版本验证,并保证输入的字节流能正确解析并存储于方法区之内
  • 元数据验证:是否有父类(除Object外都应有父类)、是否继承了不被允许的类(final修饰)、是否实现了父类或接口中要求实现的方法、类的字段方法是否与父类产生矛盾(覆盖final字段、不合规的方法重载)
  • 字节码验证:保证指令不会跳转到方法体以外的字节码指令、类型转换是否有效、
  • 符号引用验证
  1. 准备:为类中定义的变量(静态变量、static修饰)分配内存并设置初始值
  2. 解析:将常量池内的符号引用替换为直接引用
  3. 初始化:执行类构造器clinin()方法(javac自动生成,先执行父类的,故Object最先,仅执行一次)
  4. 使用
  5. 卸载

双亲委派模型:

BootStrap ClassLoader(启动类加载器):负责加载<JAVA_HOME>\lib目录(按文件名识别,如rt.jar、tools.jar,名字不符的放在lib目录下也不会加载)。

Extension ClassLoader(扩展类加载器):负责加载<JAVA_HOME>\lib\ext目录下,或被java.ext.dirs系统变量指定的路径下类库

Application ClassLoader(应用程序类加载器):加载用户类路径上的所有类库。

工作过程:类加载收到类加载的请求,先把请求层层委派给父类加载器,当父类加载器无法完成类加载请求时,子类才会尝试自行加载。

优点:保证程序稳定,如Object。

即时编译(JIT):

触发条件:编译的目标对象均是整个方法体,执行入口不同。

  1. 被多次调用的方法。
  2. 被多次调用的循环体。

为何不直接编译成机器码?

即时编译器编译本地代码需要占用程序运行时间,编译出优化程度越高的代码消耗时间越长,且想要编译出优化程度高的代码还需要收集监控信息。

编译器优化技术:

  1. 方法内联:把目标方法的代码复制到发起调用的方法中,避免真实的方法调用。
  2. 逃逸分析:分析对象动态作用域,当一个对象在方法里被定义,如作为参数传递到其他方法,称为方法逃逸;如赋值给其他线程中的实例变量,称为线程逃逸;仅作用在本方法内,称为从不逃逸。

优化方式:

栈上分配:完全不会逃逸的局部对象和不会逃逸出线程的对象直接在栈上分配,随方法结束自动销毁,减少垃圾收集器压力。

标量替换:无法在分解为更小的数据称为“标量”(如:基本数据类型、reference类型等),可继续拆分的称为“聚合量”(java中的对象)。如果一个对象从不逃逸,且整个对象可被拆散,则程序执行时可不创建对象,直接在栈上创建被整个方法使用的成员变量。

同步消除:如果变量确定不会逃逸出线程,无法被其他线程访问,那么这个变量的读写就不会有竞争,对这个变量实施的同步措施就可以安全的消除。

3、公共子表达式消除:如果一个表达式E之前已经计算过了,并且从先前的计算到现在E中的变量值均未发生变化,E的这次出现就称为公共子表达式,本次则不再计算,直接使用之前计算过的值。(如:int d = (c*b)*12 + a + (a+ b*c) --> int d = E*12 +a +(a+E) --> int d = E*13 + a + a )

4、数组边界检查消除:根据数据流判断取值范围是否在数组范围内,在范围内的则可消除边界检查;null值检测和除数为0检测进行隐式异常处理,虚拟机注册一个Segment Fault信号的异常处理器,null值或除数为0时转到异常处理器恢复中断并抛出异常。

线程状态:新建、运行、无限期等待、限期等待、阻塞、结束

  新建、就绪、运行、阻塞、结束

线程安全:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

synchronized关键字经过javac编译后,会在同步块的前后分别形成monitorenter、monitorexit两个字节码指令,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。monitor 的本质是依赖于底层操作系统的 Mutex互斥。

锁优化:

适应性自旋:自旋时间不再固定,由前一次在同一个锁上的自旋时间及拥有者的状态来决定的。如果在同一个对象上自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机认为这次自旋很有可能成功,进而允许自旋等待持续较长时间;如果对某个锁,自旋很少成功获得锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,避免浪费处理器资源。

锁消除:虚拟机即时编译器在运行时检测到某段需要同步到代码不存在共享数据竞争而实施的一种对锁进行消除的优化措施,主要依据来源于逃逸分析。

锁粗化:如果一系列的连续操作都是对同一个对象反复加锁和解锁,即时没有线程竞争,频繁的进行互斥操作也会导致不必要的性能损耗,虚拟机针对这种情况会将锁同步的范围扩展(粗化)到整个操作序列的外部。(如:连续执行stringBuffer.append())

轻量级锁:在多线程竞争不激烈的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。需要注意的是轻量级锁并不是取代重量级锁,而是在大多数情况下同步块并不会出现严重的竞争情况,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。所以偏向锁是认为环境中不存在竞争情况,而轻量级锁则是认为环境中不存在竞争或者竞争不激烈,所以轻量级锁一般都只会有少数几个线程竞争锁对象,其他线程只需要稍微等待(自旋)下就可以获取锁,但是自旋次数有限制,如果自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

偏向锁:为了在无多线程竞争的情况下尽量减少不必须要的轻量级锁执行路径。其实在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,所以引入偏向锁就可以减少很多不必要的性能开销和上下文切换。偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。偏向锁在 JDK 6 及之后版本的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值