4.1.6 jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者
javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack命令格式:
jstack [ option ] vmid
jstack -l 26455
jstack 定位线程状况,重点关注:WAITING BLOCKED
eg.
waiting on <0x0000000088ca3310> (a java.lang.Object)
假如有一个进程中100个线程,很多线程都在waiting on ,一定要找到是哪个线程持有这把锁
怎么找?搜索jstack dump的信息,找 ,看哪个线程持有这把锁RUNNABLE
4.1.7 基础故障处理工具总结
表4-12 性能监控和故障处理工具
4.2 可视化故障处理工具
4.2.1 JHSDB:基于服务性代理的调试工具
4.1.2 JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-ment
Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信收集和参数动态调整。JMX是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以运行于虚拟机之上的软件中,典型的如中间件大多也基于JMX来实现管理与监控。虚拟机对JMXMBean的访问也是完全开放的,可以使用代码调用API、支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问
连接
实际状况
4.1.3 JVisualVM:多合-故障处理工具
4.1.4 Java Mission Control:可持续在线的监控工具
启动后JMC的主界面如图4-24所示。
五、调优案例与实战、
5.1 调优,从规划开始
- 调优,从业务场景开始,没有业务场景的调优都是耍流氓
- 无监控(压力测试,能看到结果),不调优
- 步骤:
- 熟悉业务场景(没有最好的垃圾回收器,只有最合适的垃圾回收器)
- 响应时间、停顿时间 [CMS G1 ZGC] (需要给用户作响应)
- 吞吐量 = 用户时间 /( 用户时间 + GC时间) [PS]
- 选择回收器组合
- 计算内存需求(经验值 1.5G 16G)
- 选定CPU(越高越好)
- 设定年代大小、升级年龄
- 设定日志参数
- -Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
- 或者每天产生一个日志文件
- 观察日志情况
- 案例1:垂直电商,最高每日百万订单,处理订单系统需要什么样的服务器配置?
这个问题比较业余,因为很多不同的服务器配置都能支撑(1.5G 16G)
1小时360000集中时间段, 100个订单/秒,(找一小时内的高峰期,1000订单/秒)
经验值,
非要计算:一个订单产生需要多少内存?512K * 1000 500M内存
专业一点儿问法:要求响应时间100ms
压测!
- 案例2:12306遭遇春节大规模抢票应该如何支撑?
12306应该是中国并发量最大的秒杀网站:
号称并发量100W最高
CDN -> LVS -> NGINX -> 业务系统 -> 每台机器1W并发(10K问题) 100台机器
普通电商订单 -> 下单 ->订单系统(IO)减库存 ->等待用户付款
12306的一种可能的模型: 下单 -> 减库存 和 订单(redis kafka) 同时异步进行 ->等付款
减库存最后还会把压力压到一台服务器
可以做分布式本地库存 + 单独服务器做库存均衡
大流量的处理方法:分而治之
- 怎么得到一个事务会消耗多少内存?
- 弄台机器,看能承受多少TPS?是不是达到目标?扩容或调优,让它达到
- 用压测来确定
5.2 优化环境
- 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了
- 为什么原网站慢?
很多用户浏览数据,很多数据load到内存,内存不足,频繁GC,STW长,响应时间变慢 - 为什么会更卡顿?
内存越大,FGC时间越长 - 咋办?
PS -> PN + CMS 或者 G1
- 系统CPU经常100%,如何调优?(面试高频)CPU100%那么一定有线程在占用系统资源,
- 找出哪个进程cpu高(top)
- 该进程中的哪个线程cpu高(top -Hp)
- 导出该线程的堆栈 (jstack)
- 查找哪个方法(栈帧)消耗时间 (jstack)
- 工作线程占比高 | 垃圾回收线程占比高
- 系统内存飙高,如何查找问题?(面试高频)
- 导出堆内存 (jmap)
- 分析 (jhat jvisualvm mat jprofiler … )
- 如何监控JVM
- jstat jvisualvm jprofiler arthas top…
解决JVM运行中的问题
一个案例理解常用工具
- 测试代码:
package com.mashibing.jvm.gc; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 从数据库中读取信用数据,套用模型,并把结果进行记录和传输 */ public class T15_FullGC_Problem01 { private static class CardInfo { BigDecimal price = new BigDecimal(0.0); String name = "张三"; int age = 5; Date birthdate = new Date(); public void m() {} } private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50, new ThreadPoolExecutor.DiscardOldestPolicy()); public static void main(String[] args) throws Exception { executor.setMaximumPoolSize(50); for (;;){ modelFit(); Thread.sleep(100); } } private static void modelFit(){ List<CardInfo> taskList = getAllCardInfo(); taskList.forEach(info -> { // do something executor.scheduleWithFixedDelay(() -> { //do sth with info info.m(); }, 2, 3, TimeUnit.SECONDS); }); } private static List<CardInfo> getAllCardInfo(){ List<CardInfo> taskList = new ArrayList<>(); for (int i = 0; i < 100; i++) { CardInfo ci = new CardInfo(); taskList.add(ci); } return taskList; } }
- java -Xms200M -Xmx200M -XX:+PrintGC com.mashibing.jvm.gc.T15_FullGC_Problem01
- 一般是运维团队首先受到报警信息(CPU Memory)
- top命令观察到问题:内存不断增长 CPU占用率居高不下
- top -Hp 观察进程中的线程,哪个线程CPU和内存占比高
- jps定位具体java进程
jstack 定位线程状况,重点关注:WAITING BLOCKED
eg.
waiting on <0x0000000088ca3310> (a java.lang.Object)
假如有一个进程中100个线程,很多线程都在waiting on ,一定要找到是哪个线程持有这把锁
怎么找?搜索jstack dump的信息,找 ,看哪个线程持有这把锁RUNNABLE
作业:1:写一个死锁程序,用jstack观察 2 :写一个程序,一个线程持有锁不释放,其他线程等待 - 为什么阿里规范里规定,线程的名称(尤其是线程池)都要写有意义的名称
怎么样自定义线程池里的线程名称?(自定义ThreadFactory) - jinfo pid
- jstat -gc 动态观察gc情况 / 阅读GC日志发现频繁GC / arthas观察 / jconsole/jvisualVM/ Jprofiler(最好用)
jstat -gc 4655 500 : 每个500个毫秒打印GC的情况
如果面试官问你是怎么定位OOM问题的?如果你回答用图形界面(错误)
1:已经上线的系统不用图形界面用什么?(cmdline arthas)
2:图形界面到底用在什么地方?测试!测试的时候进行监控!(压测观察) - jmap - histo 4655 | head -20,查找有多少对象产生
- jmap -dump:format=b,file=xxx pid :
线上系统,内存特别大,jmap执行期间会对进程产生很大影响,甚至卡顿(电商不适合)
1:设定了参数HeapDump,OOM的时候会自动产生堆转储文件
2:很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
3:在线定位(一般小点儿公司用不到) - java -Xms20M -Xmx20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError com.mashibing.jvm.gc.T15_FullGC_Problem01
- 使用MAT / jhat /jvisualvm 进行dump文件分析
https://www.cnblogs.com/baihuitestsoftware/articles/6406271.html
jhat -J-mx512M xxx.dump
http://192.168.17.11:7000
拉到最后:找到对应链接
可以使用OQL查找特定问题对象 - 找到代码的问题
六、类文件结构
6.1 Class类文件的结构
6.2 字节码指令简介
不会的直接去官网看比较靠谱https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit
七、虚拟机类加载机制
7.1 概述
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
7.2 类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候。
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
- 调用一个类型的静态方法的时候。
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
7.3 类加载的过程
类加载分为五个阶段,即加载、验证、准备、解析、初始化五阶段。
7.3.1 加载阶段
在加载阶段Java虚拟机需要完成以下三件事
- 通过一个类的全限名来获取此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
7.3.2 验证阶段
验证是连接阶段的第一步,这一阶段的目的就是确保Class文件的字节流信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
7.3.3 准备阶段
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
关于准备阶段,还有两个容易产生混淆的概念笔者需要着重强调,首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value = 123;
那变量value在准备阶段过后的初始值为0而不是123因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。表7-1列出了Java中所有基本数据类型的零值。
表7-1 基本数据类型的零值
上面提到在“通常情况”下初始值是零值,那言外之意是相对的会有某些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,假设上面类变量value的定义修改为:
public static final int value = 123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value赋值为123。
7.3.4 解析阶段
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
7.3.5 初始化阶段
类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器()方法的过程。()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物,但我们非常有必要了解这个方法具体是如何产生的,以及()方法执行过程中各种可能会影响程序运行行为的细节,这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作 [1] 。
·()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
public class Test { static { i = 0; // 给变量复制可以正常编译通过 System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; }
·()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的()方法执行前,父类的(方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。
由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如代码清单7-6中,字段B的值将会是2而不是1。
代码清单7-6
static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); }
7.4 类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
类加载器可以说是Java语言的一项创新,它是早期Java语言能够快速流行的重要原因之一。类加载器最初是为了满足Java Applet的需求而设计出来的,在今天用在浏览器上的Java Applet技术基本上已经被淘汰 [1] ,但类加载器却在类层次划分、OSGi、程序热部署、代码加密等领域大放异彩,成为Java技术体系中一块重要的基石,可谓是失之桑榆,收之东隅。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
双亲委派机制应用在类加载的过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQlyf3Kx-1597113521258)(%E6%B7%B1%E5%85%A5%E7%A0%94%E7%A9%B6JVM%E4%B8%93%E9%A2%98.assets/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E8%BF%87%E7%A8%8B%E2%80%94%E2%80%94%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6.png)]
类加载器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mdawesb4-1597113521261)(%E6%B7%B1%E5%85%A5%E7%A0%94%E7%A9%B6JVM%E4%B8%93%E9%A2%98.assets/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8.png)]
- 双亲委派,主要出于安全来考虑,比如如果自己定义了一个java.lang.String的类然后打成jar包,给第三方使用,如果第三方用这个来存储账号和密码的时候,那么jar中就可以获得用户的账号和密码,这样子用户的账号和密码就不仅仅只存在于数据库当中了,这样是不安全的。
- 父加载器不是“类加载器的加载器”也不是“类加载器的父类加载器”。父加载器并不是指的是父类,这里和java中的继承关系不一样,在类加载器中会有一个parent属性,这里是由jdk内部帮你赋值的,当你加载一个类的时候由于双亲委派(源码中可以看到)所以会先让parent对应的加载器进行加载
历史上有三次打破双亲委派
打破双亲委派重写loadClass方法
package com.zhou.jvm.loading.testthree; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * @author zhouyanxiang * @create 2020-07-2020/7/27-21:32 * 破坏双亲委派机制 */ public class ClassLoadOverride { private static class MyLoader extends ClassLoader { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { // File f = new File("C:/work/ijprojects/JVM/out/production/JVM/" + name.replace(".", "/").concat(".class")); String filePath = "D:\\WorkSpace\\IdeaWorkSpace\\JVM-zhou-master\\out\\production\\JVM-zhou-master\\"; File f = new File(filePath + name.replace(".", "/").concat(".class")); if(!f.exists()) { return super.loadClass(name); } try { InputStream is = new FileInputStream(f); byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { e.printStackTrace(); } return super.loadClass(name); } } public static void main(String[] args) throws Exception { MyLoader m = new MyLoader(); Class clazz = m.loadClass("com.zhou.jvm.Hello"); m = new MyLoader(); Class clazzNew = m.loadClass("com.zhou.jvm.Hello"); System.out.println(clazz == clazzNew); } }
- LazyLoading 五种情况
- –new getstatic putstatic invokestatic指令,访问final变量除外
–java.lang.reflect对类进行反射调用时
–初始化子类的时候,父类首先初始化
–虚拟机启动时,被执行的主类必须初始化
–动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
- ClassLoader的源码实现双亲委派的源码loadClass方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
- findInCache -> parent.loadClass -> findClass()
- 自定义类加载器
- extends ClassLoader
- overwrite findClass() -> defineClass(byte[] -> Class clazz)
- 加密
- 第一节课遗留问题:parent是如何指定的,打破双亲委派
- 用super(parent)指定
- 双亲委派的打破
- 如何打破:重写loadClass()
- 何时打破过?
- JDK1.2之前,自定义ClassLoader都必须重写loadClass()
- ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
- 热启动,热部署
- osgi tomcat 都有自己的模块指定classloader(可以加载同一类库的不同版本)
- 混合执行编译执行 解释执行
- 检测热点代码:-XX:CompileThreshold = 10000
八、虚拟机字节码执行引擎
Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual MachineStack) [1] 的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,能从Class文件格式的方法表中找到以上大多数概念的静态对照物。每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
在不同的虚拟机实现中,执行引擎在执行字节码的时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择 [1] ,也可能两者兼备,还可能会有同时包含几个不同级别的即时编译器一起工作的执行引擎。但从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。
这里一般会牵涉到局部变量表和操作数栈,涉及到的常见问题就是i++和++i的区别,详情可以参照这篇博客https://blog.csdn.net/qq_41688840/article/details/107825771
8.1 局部变量表 Local Variables Table
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。
8.2 操作数栈(Openand Stack)
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
图8-2 两个栈帧之间的数据共享
8.3 动态链接
每个栈帧都包含一个指向运行时常量池 [1] 中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。
8.4 方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。
另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。
8.5 解释执行
九、类加载以及子系统的案例
具体参考git代码地址:https://gitee.com/zyxscuec/JVM.git
十、Java内存模型与线程
10.1 硬件层数据一致性
协议很多
intel 用MESI
https://www.cnblogs.com/z00377750/p/9180644.html
现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁
读取缓存以cache line为基本单位,目前64bytes
位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题
伪共享问题:JUC/c_028_FalseSharing
使用缓存行的对齐能够提高效率
10.2 乱序问题
CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系
https://www.cnblogs.com/liushaodong/p/4777308.html
写操作也可以进行合并
https://www.cnblogs.com/liushaodong/p/4777308.html
JUC/029_WriteCombining
乱序执行的证明:JVM/jmm/Disorder.java
原始参考:https://preshing.com/20120515/memory-reordering-caught-in-the-act/
如何保证特定情况下不乱序
硬件内存屏障 X86
sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序
JVM级别如何规范(JSR133)
LoadLoad屏障:
对于这样的语句Load1; LoadLoad; Load2,
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:
对于这样的语句Store1; StoreStore; Store2,
在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:
对于这样的语句Load1; LoadStore; Store2,
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:
对于这样的语句Store1; StoreLoad; Load2,
在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
volatile的实现细节
- 字节码层面
ACC_VOLATILE - JVM层面
volatile内存区的读写 都加屏障
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
- OS和硬件层面
https://blog.csdn.net/qq_26222859/article/details/52235930
hsdis - HotSpot Dis Assembler
windows lock 指令实现 | MESI实现
synchronized实现细节
- 字节码层面
ACC_SYNCHRONIZED
monitorenter monitorexit - JVM层面
C C++ 调用了操作系统提供的同步机制 - OS和硬件层面
X86 : lock cmpxchg / xxx
https😕/blog.csdn.net/21aspnet/article/details/88571740