深入理解java虚拟机

运行时内存区域

总览

运行时数据区

详细介绍

程序计数器

程序计数器是一块较小的内存空间,它可以看做当前线程所执行的字节码行号记录器。

Java虚拟机栈

Java虚拟机栈是线程私有的,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方案执行时,都会同步创建一个栈帧 用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧 从虚拟机栈入栈和出栈的过程。

本地方法栈

本地方法栈 和 Java虚拟机栈的作用类似,只是本地方法栈为本地方法(native)服务。

Java堆

Java堆是所有线程共享的内存区域,在虚拟机启动时创建。此内存的唯一作用就是存放对象实例,Java中“几乎所有”对象都在这里分配内存。

方法区

方法区 和 Java堆一样,是各个线程共享的内存区域,他用于存储虚拟机加载的类型信息、常量、静态常量、及时编译器编译后的代码缓存等数据。

HotSpot虚拟机对象揭秘

对象创建

在这里插入图片描述

原始链接

对象的内存布局

在HotSpot虚拟机中,对象在堆内存中存储布局可以划分为三个部分:对象头(Header)、示例数据(Instance Data)和对齐填充(Padding)。
对象头有两类信息。第一种是存储对象自身运行时数据,如hash值,GC分代年龄,锁状态。 第二种是类型指针,指针指向其类对象。

对象的访问定位

通过句柄访问对象
通过直接指针访问对象

通过句柄访问对象通过直接指针访问对象
优点对象发生移动时,只需要修改句柄中指针就好,修改性能高访问效率高
缺点访问效率低对象发生移动时,需要修改所有应用该对象的reference

垃圾回收策略与内存分配策略

实现一个垃圾回收期,主要考虑3个问题。

  1. 哪些内存需要回收
  2. 什么时候回收
  3. 如何回收

如何判断是否需要回收

引用计数法

实现:
在对象中添加一个引用计数器,每当有一个地方引用他,计数器数值就加一;引用失效时,计数器就减一。
缺点:循环引用的对象无法被回收。

可达性分析

实现:
通过一系列“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用链开始向下搜索,搜索过程所走的路径成为“引用链”,如果某个对象到GC Roots之间没有“引用链”,说明这个对象不可达,可以判定为可回收对象。

在这里插入图片描述

GC Roots有哪些:

  1. Java虚拟机栈中应用的对象,例如:参数、局部变量
  2. 类静态变量中引用的对象。
  3. 同步锁持有的对象

常见的垃圾回收算法

标记-清除算法

标记-清除算法

标记-复制算法

标记-复制算法

标记-整理算法

标记-整理算法

优缺点总结

GC算法优点缺点存活对象移动内存碎片适用场景
标记清除不需要额外空间两次扫描,耗时严重NY老年代
复制没有标记和清除需要额外空间YN新生代
标记整理没有内存碎片需要移动对象的成本YN老年代

HotSpot的算法实现

//todo 看不懂

经典的垃圾回收器

垃圾回收器介绍

参考:https://www.cnblogs.com/cxxjohnson/p/8625713.html

Seriral收集器

Seriral收集器
GC时,会暂时所以用户线程。

ParNew收集器

ParNew收集器

Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

Serial Old收集器

Serial Old是 Serial收集器的老年代版本。采用"标记-整理"算法。
在这里插入图片描述

Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本。采用"标记-整理"算法;

CMS收集器

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;

  1. 初始标记(CMS initial mark)
    仅标记一下GC Roots能直接关联到的对象;速度很快;但需要"Stop The World";
  2. 并发标记(CMS concurrent mark)
    进行GC Roots Tracing的过程;应用程序也在运行;并不能保证可以标记出所有的存活对象;
  3. 重新标记(CMS remark)
    为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率;
  4. 并发清除(CMS concurrent sweep)
    回收所有的垃圾对象;

在这里插入图片描述
优点:

缺点:

  1. 并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。

缺点:

  1. 对CPU资源非常敏感
    并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
  2. 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
    2.1
    在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;也要可以认为CMS所需要的空间比其他垃圾收集器大;
    2.2
    如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
  3. 产生大量内存碎片
    产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。

G1收集器

ZGC收集器

https://www.jianshu.com/p/92fd7fdc77ac

故障处理工具

//todo

虚拟机类加载机制

类加载时机

(1)new 关键字实例化一个对象时。
(2)读取或者设置一个静态变量时。
(3)调用一个静态方法。
(4)使用java.lang.reflect包,进行反射调用时。
(5)子类初始化时,父类没有初始化,会先触发父类的初始化。

类加载过程

类加载过程

图片原始链接

类加载器

类的定位:加载他的类的加载器 + 类的全限定名。
双亲委派模式:
双亲委派模式
在这里插入图片描述
优点:
避免重复加载 + 避免核心类篡改

虚拟机字节码执行系统

//todo

类加载与执行子系统案例

Tomcat加载机制

参考:https://www.cnblogs.com/xing901022/p/4574961.html
https://www.jianshu.com/p/abf6fd4531e7

一句话概括:
每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
在这里插入图片描述

JDK动态代理原理

一句话概括:修改字节码,把代理类的代码接上去,并重新加载。
https://www.jianshu.com/p/84ffb8d0a338
https://www.cnblogs.com/yeyang/p/10087293.html

虚拟机运行时栈帧结构

栈帧结构

在这里插入图片描述
每个栈帧中都包含一个执行运行时常量池中改栈帧所属方法的引用,持有这个方法应用是为了支持方法调用过程中的动态链接。

方法调用

方法调用并非等同于方法中的代码被执行,方法调用阶段唯一的任务是确定被调用方法的版本(即调用哪个方法),暂时未涉及方法内部具体执行过程。

一切方法调用在class文件中存储的是符号引用,而不是实际运行时的方法入口地址(也就是直接引用)。

方法调用分为两种:解析和分派
解析:
在类加载阶段,会将一部分符号应用转化为直接引用,这种解析能够成立的前提是 方法在程序运行前就有一个可确定的调用版本,并且这个版本在运行时不会发生改变。 这个方法主要有静态方法和私有方法。

分派:
分派分为两种,一种是静态分派(重载)、一种是动态分派(重写)。
静态分派在编译时就能确定了,其实可以说出 解析。
动态分派在运行时是才能确定方法的版本。
在这里插入图片描述
在这里插入图片描述

执行动态分派的实现步骤:
在这里插入图片描述

动态类型语言

定义:动态类型语言的类型检查的主题过程是在运行过程而不是编译期。
动态类型的语言有JavaScript、Python。

解释执行引擎

javac编译器输出的字节码指令流,基本上是一种基于栈的指令集架构。与之相对的是,基于寄存器的指令集。
在这里插入图片描述

在这里插入图片描述
基于栈的指令集的优点是可移植,因为寄存器有多种品牌,所以有多种指令集。基于栈的指令集的缺点是执行速度相对慢一点,因为出栈入栈本身就会产生很多指令。

基于栈的解释执行过程(理论模型)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

编译器优化技术

Java程序最初都是通过解释器进行解释执行的,但虚拟机发现某个方法或者代码块运行特别频繁,就会把代码定位“热点代码”,为了提供“特点代码”的执行效率,在运行时,虚拟机会把这些代码编译成本地机器码。

编译器优化技术

方法内联

一句话概括:把被调用的方法的代码“原封不动”的复制到发起调用的方法之中,避免发生真实的调用。
具体实现:
非虚方法,可以直接通过复制代码的方式来实现。 但是虚方法(多态)有多种实现方式,所以无法确定具体使用哪个版本的方法。
虚方法通过CHA (Class Hierarchy Analysis)来判断:

  • 当目标方法的接收对象只有一个版本时,进行方法内联,如果程序执行过程中方法的接受对象的类继承关系一直没有发生改变,则这种优化可以持续,否则抛弃已经编译的代码继续解释执行。
  • 当方法接收对象有多个版本,编译器会在方法入口之前建立一个内联缓存,第一次方法调用记录方法接收者的版本,以后每次方法调用进行接收者版本比较,如果一致,方法内联可以持续,否则取消内联,改为查询虚方法表进行方法分派。
  • 方法内联属于“激进优化”,有小概率事件会导致虚拟机从“逃生门”回到解释状态重新执行。

逃逸分析

相关概念:

  • 方法逃逸:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;
  • 线程逃逸:甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;

一句话概括:如果一个对象被分析出来一定逃不出这个这个方法,那就把堆分配转换成栈分配。

具体优化措施:

  • 栈上分配(Stack Allocations): 如果一个对象被分析出来一定逃不出这个这个方法,那就把堆分配转换成栈分配。
  • 标量替换(Scalar Replacement): 如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换
  • 同步消除(Synchronization Elimination):如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,对这个变量实施的同步措施也就可以安全地消除掉。

公共子表达式消除

如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对他进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。
例如:

int d = (c*b)*12+a+(a+b*c);

"cb"与"bc"是一样的表达式,而且在计算期间b与c的值是不变的。因此,这条表达式就可能被视为:

int d = E*12+a+(a+E);

Java内存模式

主内存与工作内存

Java内存模型的主要目标是定义程序中变量的访问规则。即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。需要注意的是这里的变量跟我们写java程序中的变量不是完全等同的。这里的变量是指实例字段,静态字段,构成数组对象的元素,但是不包括局部变量和方法参数(因为这是线程私有的)。这里可以简单的认为主内存是java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。

Java内存模型中涉及到的概念有:

主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
在这里插入图片描述

volatile关键字的特殊规则

  • valatile类型的变量保证对所有线程的可见性(实现方式:volatile)
  • volatile变量禁止指令重排序优化

原子性、可见性、有序性

Java线程与线程

java线程实现

todo
以HotSpot虚拟机为例子,它的每个Java线程都一一映射到操作系统的线程上,没有中间结构。HotSpot虚拟机不干预线程的调度。

Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
协同式调度:
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。协同式多线程的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。它的坏处也很明显:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。

抢占式调度
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度。

在这里插入图片描述
“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值