JVM(上篇——①)
一、JVM和Java体系结构
1、前言
1.1、你是否也遇到过这些问题?
运行着的线上系统突然卡死,系统无法访问,甚至直接OOM(Out of Memory)!想解决线上MGC问题,但却无从下手
新项目上线,对各种JVM参数设置一脸茫然,直接默认吧,然后就JJ了
每次面试之前都要重新背一遍VM的一些原理概念性的东西,然而面试官
却经常问你在实际项目中如何调优VM参数,如何解决GC、OOM等问题,
一脸懵逼。
大部分Java开发人员,除会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。
1.2、开发人员如何看待上层框架
一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,这其实是一种本末倒置的“病态”。
如果我们把核心类库的API比做数学公式的话,那么Java虚拟机的知识就好比公式的推导过程。
计算机系统体系对我们来说越来越远,在不了解底层实现方式的前提下,通过高级语言很容易编写程序代码。但事实上计算机并不认识高级语言
1.3、架构师每天都在思考什么?
- 应该如何让我的系统更快?
- 如何避免系统出现瓶颈?
知乎上有条帖子:应该如何看招聘信息,直通年薪50万+?
- 参与现有系统的性能优化,重构,保证平台性能和稳定性
- 根据业务场景和需求,决定技术方向,做技术选型
- 能够独立架构和设计海量数据下高并发分布式解决方案,满足功能和非功能需求
- 解决各类潜在系统风险,核心功能的架构与代码编写
- 分析系统瓶颈,解决各种疑难杂症,性能调优等
1.4、我们为什么要学习JVM?
面试的需要(BATJ、TMD、PKQ等面试都爱问)
中高级程序员必备技能
项目管理、调优的需要
追求极客的精神
比如:垃圾回收算法、JIT、底层原理
1.5、Java VS C++
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hdtmenvt-1682652254702)(https://gitee.com/lin-xugeng/image2/raw/master/img/202304191407756.png)]
垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率,但是,垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高扩展性应用和诊断运行时问题的基础,也是Java工程师进阶的必备能力。
2、面向人群及参考书名
面对人群
拥有一定开经验的Java平台开发人员
软件设计师、架构师
系统调优人员
有一定的Java编程基础并希望进一步理解Java的程序员
虚拟机爱好者,JVM实践者
参考书目
Java SE Specifications (oracle.com)
3、Java及JVM简介
3.1、TIOBE语言热度排行榜
https://www.tiobe.com/tiobe-index/
Java是目前应用最为广泛的软件开发平台之一。随着Java以及ava社区的不断壮大,
Java也早已不再是简简单单的一门计算机语言了,它更是一个平台、一种文化、一
个社区。
作为一个平台,Java虚拟机扮演着举足轻重的作用。
- Groovy、Scala、JRuby、Kot1in等都是Java平台的部分
作为一种文化,Java几乎成为了“开源”的代名词。
-
第三方开源软件和框架。如Tomcat、Struts,MyBatis,Spring等。
-
就连JDK和JVM自身也有不少开源的实现,如OpenJDK、Harmony。
作为一个社区,Java拥有全世界最多的技术拥护者和开源社区支持,有数不清的论坛和资料。从桌面应用软件、嵌入式开发到企业级应用、后台服务器、中间件,可以看到Java的身影。其应用形式之复杂、参与人数之众多也令人咋舌。
3.2、java:跨平台的语言
3.3、Java虚拟机规范
Java虚拟机是Java平台的基石。它是负责硬件和操作系统的技术组件独立性,其编译代码的小尺寸,以及它有保护用户远离恶意程序的能力。
Java虚拟机是一种抽象的计算机器。就像一个真正的计算机机器,它有一个指令集,并在运行时操作不同的内存区域。使用虚拟实现编程语言是相当普遍的。最著名的虚拟机可能是UCSD的P-Code机帕斯卡。
3.4、JVM:跨语言的平台
JVM(Java虚拟机)是Java程序的运行环境,它是一个虚拟的计算机,可以在不同的平台上运行Java程序。JVM负责将Java程序编译后的字节码解释执行,并提供了内存管理、垃圾回收、安全性等功能。JVM的核心是Java解释器,它可以将字节码转换为机器码并执行。JVM还包括了Java类库、Java运行时环境和Java开发工具等。
随着Java7的正式发布,Java虚拟机的设计者们通过JSR-292规范基本实现在Java虚拟机平台上运行非Java语言编写的程序。
Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,它只关心“字节码”文件。也就是说Jav虚拟机拥有语言无关性,并不会单纯地与Java语言“终身绑定”,只要其他编程语言的编译结果满足并包含Java虚拟机的内部指令集、符号表以及其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行。
Java不是最强大的语言,但是VM是最强大的虚拟机.
3.5、字节码
我们平时说的iava字节码,指的是用java语言编译成的字节码。准确的说任何能在jvm平台上执行的字节码格式都是一样的。所以应该统称为:jvm字节码。
不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同
的JVM上运行。
Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式c1ass文件格式所关联,class文件中包含了Java虚拟机指令集(或者称为字节码、Bytecodes)和符号表,还有一些其他辅助信息。
3.6、多语言混合编程
Java平台上的多语言混合编程正成为主流,通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。
试想一下,在一个项目之中,并行处理用clojure语言编写,展示层使用JRuby/Rails,中间层则是Java,每个应用层都将使用不同的编程语言来完成,而且,接口对每一层的开发者都是透明的,各种语言之间的交互不存在任何困难,就像使用自己语言的原生AP工一样方便,因为它们最终都运行在一个虚拟机之上。
对这些运行于Java虚拟机之上、Java之外的语言,来自系统级的、底层的支持正在迅速增强,以JSR-292为核心的一系列项目和功能改进(如DaVinci、 Machine项月、Nashorn引擎、InvokeDynamic指令、java.lang.invoke包等),推动Java虚拟机从“Java语言的虚拟机”向“多语言虚拟机”的方向发展。
3.7、如何真正搞懂JVM
Java虚拟机非常复杂,要想真正理解它的工作原理,最好的方式就是自己动手编写一个!
自己动手写一个Java虚拟机,难吗?
天下事有难易乎?
为之,则难者亦易矣;不为,则易者亦难矣
4、Java发展的重大事件
1990年,在sun计算机公司中,由Patrick Naughton、MikeSheridan及
James Gosling领导的小组GreenTeam,开发出的新的程序语言,命名为oak,
后期命名为Java
1995年,Sun正式发布Java和HotJava产品,Java首次公开亮相。
1996年1月23日Sun Microsy3tems发布了JDK1.0。
1998年,JDK1.2版木发布。同时,Sun发布了JsP/Servlet、EJB规范,以及将Java分成了J2EE、J2sE和J2ME。这表明了Java开始向企业、桌面应用和移动设备应用3大领域挺进。
2000年,JDK1.3发布,Java HotSpot Virtual Machine.正式发布,成为Java的默认虚拟机。
2002年,JDK1.4发布,古老的c1as3ic虚拟机退出历史舞台。
2003年年底,Java平台的Scala正式发布,同年Groovy也加入了Java阵营。
2004年,JDK1.5发布。同时JDK1.5改名为JavaSE5.0。
2006年,JDK6发布。同年,Java开源并建立了OpenJDK。顺理成章,Hotspot
虚拟机也成为了OpenJDK中的默认虚拟机。
2007年,ava平台迎来了新伙伴Clojure。
2008年,Oracle收购了BEA,得到了JRockit虚拟机。
2009年,Twitter宣布把后台大部分程序从Ruby迁移到sca1a,这是Java平台的又一次
大规模应用。
20l0年,Orac1e收购了Sun,获得Java商标和最具价值的HotSpot虚拟机。此时,Oracle拥有市场占用率最高的两款虚拟机HotSpot和JRockit,并计划在未来对它们进行整合:HotRockit
2011年,JDK7发布。在JDK1.7u4中,正式启用了新的垃圾回收器G1。
2017年,JDK9发布。将G1设置为默认GC,替代CMS
同年,IBM的J9开源,形成了现在的Open J9社区
2018年,Android的Java侵权案判决,Google.赔偿Ocacle计88亿美元
同年,Oracle宣告JavaEE成为历史名词,JDBC、JMS、Servlet赠予Eclipse基金会
同年,JDK11发布,LTS版木的JDK,发布革命性的ZGC,调整JDK授权许可
2019年,JDK12发布,加入RedHat领导开发的Shenandoah GC
Open JDK和Oracle JDK
在JDK11之前,OracleJDK中还会存在一些openJDK中没有的、闭源的功能。但在JDK11中,我们可以认为openJDK和oracleJDK代码实质上已经完全一致的程度。
5、虚拟机与Java虚拟机
5.1、虚拟机
所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
-
大名鼎鼎的visual Box,Mware.就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
-
程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。
无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。
5.2、Java虚拟机
Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回
器,以及可靠的即时编译器。
Java技术的核心就是Java虚拟机(JVM,Java virtual Machine),
因为所有的Java程序都运行在Java虚拟机内部。
作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点
一次编译,到处运行
自动内存管理
自动垃圾回收功能
5.3、JVM的位置
6、JVM的整体结构
HotSpot VM是目前市面上高性能虚拟机的代表作之一。
它采用解释器与即时编译器并存的架构。
在今天,Java程序的运行性能早己脱胎换骨,己经达到了可以和C/C++程序一较高下的地
步。
方法区和堆是多线程共享一份的,而Java栈、本地方法栈、程序计数器是每个线程都有独一份的。
7、Java代码执行流程
8、JVM的架构模型
Hotspot是基于栈的指令集架构,也被称为基于堆栈的虚拟机(Stack-based Virtual Machine)。Hotspot虚拟机将所有的计算操作都基于操作数栈进行,而不是像基于寄存器的指令集架构(Register-based Instruction Set Architecture)那样基于寄存器。这种设计使得Hotspot虚拟机的指令集更加简单,同时也更加容易实现跨平台的特性。
Java编译器输入的指令流基木上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。
具体来说:这两种架构之间的区别:
基于栈式架构的特点
- 设计和实现更简单,适用于资源受限的系统;
- 避开了寄存器的分配难题:使用零地址指令方式分配。
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
- 不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构的特点
- 典型的应用是x86的二进制指令集:比如传统的Pc以及Android的Davlik虚拟机。
- 指令集架构则完全依赖硬件,可移植性差
- 性能优秀和执行更高效:
- 花费更少的指令去完成一项操作。
- 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。方难学的
举例1:
同样执行2+3这种逻辑操作,其指令分别如下:
基于栈的计算流程(以Java虚拟机为例):
iconst_2 //常量2入栈
istore_1
iconst3 //常量3入栈
istore_2
iload_1
iload_2
iadd //常量2、3出栈,执行相加
istore_0 //结果5入栈
而基于寄存器的计算流程:
mov eax,2 //将eax寄存器的值设为2
add eax,3 //使eax寄存器的值加3
javap -v .\StackStruTest.class
命令:进行反编译
javap -V .class文件
也可以使用idea插件jclasslib
iconst_2 //常量2入栈
istore_1 //表示常量2保存在索引为1的位置上
iconst3 //常量3入栈
istore_2 //表示常量3保存在索引为2的位置上
iload_1 //加载索引为1的常量
iload_2 //加载索引为2的常量
iadd //常量2、3出栈,执行相加
istore_0 //结果5入栈
举例2:
bipush和sipush是Java字节码指令,用于将常数值推送到操作数栈中。
bipush指令用于将一个字节大小的常数值(-128到127)推送到操作数栈中。它的操作码为0x10,后面跟着一个8位有符号整数表示要推送的常数值。
sipush指令用于将一个短整型大小的常数值(-32768到32767)推送到操作数栈中。它的操作码为0x11,后面跟着一个16位有符号整数表示要推送的常数值。
这两个指令常用于编写Java字节码,而不是直接编写Java源代码。在Java编译器将源代码编译成字节码时,常量值会被推送到操作数栈中,这些指令在这个过程中被使用。
iconst_m1是Java字节码指令之一,用于将整数常量-1推送到操作数栈中。
Java虚拟机提供了一组特殊的指令,可以将整数常量-1到5快速地推送到操作数栈中,其中包括iconst_m1、iconst_0、iconst_1、iconst_2、iconst_3、iconst_4和iconst_5。
iconst_m1的操作码为0x02,表示将整数常量-1推送到操作数栈中。其他常量指令的操作码和作用也类似。
这些常量指令可以提高Java字节码的执行效率,因为它们比使用bipush或sipush指令更短,执行速度也更快。
如果要将浮点数常量推送到操作数栈中,可以使用fconst_0、fconst_1和fconst_2指令,分别用于将浮点数常量0.0、1.0和2.0推送到操作数栈中。如果要将长整数常量推送到操作数栈中,可以使用lconst_0和lconst_1指令,分别用于将长整数常量0和1推送到操作数栈中。如果要将双精度浮点数常量推送到操作数栈中,可以使用dconst_0和dconst_1指令,分别用于将双精度浮点数常量0.0和1.0推送到操作数栈中。如果要将字符串常量推送到操作数栈中,可以使用ldc指令,该指令需要一个常量池索引作为参数,该索引指向一个字符串常量在常量池中的位置。需要注意的是,每种类型的常量指令都只能用于将特定类型的常量推送到操作数栈中,不能将其他类型的常量使用这些指令推送到操作数栈中。
8.3、总结
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
时至今日,尽管嵌入式平台己经不是Java程序的主流运行平台了(准确来说应该是HotspotVM的宿主环境已经不局限于嵌入式平台了),那么为什么不将
架构更换为基于寄存器的架构呢?
将架构更换为基于寄存器的架构并不一定是最优的选择,因为:1. 兼容性问题:现有的Java程序都是基于栈式架构的虚拟机运行的,如果要将架构更换为基于寄存器的架构,需要重新编写和调整Java程序,这会带来很大的兼容性问题。2. 硬件成本问题:基于寄存器的架构需要更多的寄存器,这会增加硬件成本。此外,基于寄存器的架构需要更多的硬件支持,比如更复杂的指令集和更高的性能要求,这也会增加硬件成本。3. 性能问题:尽管基于寄存器的架构可以提高一些性能指标,但是在某些情况下,栈式架构的虚拟机也可以达到很高的性能水平。此外,现在的硬件技术和虚拟机优化技术也可以在栈式架构下实现很高的性能。综上所述,将架构更换为基于寄存器的架构并不一定是最优的选择,而现有的栈式架构的虚拟机已经可以在嵌入式平台上运行得很好了。
栈:跨平台性、指令集小、指令多;执行性能比寄存器差
9、JVM的生命周期
9.1、虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
9.2、虚拟机的执行
- 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。
- 程序开始执行时他才运行,程序结束时他就停止。
- 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
9.3、虚拟机的退出
有如下的几种情况:
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作。
- 除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。
10、JVM的发展历程
10.1、Sun Classic VM
早在1996年Java1.0版本的时候,Sun公司发布了一款名为sunClassic VM的Java虚拟机,它同时也是世界上第一款商用Java虚拟机,JDK1.4时完全被淘汰。
这款虚拟机内部只提供解释器。
如果使用JIT编译器,就需要进行外挂。但是一旦使用了JIT编译器,JIT就会接管虚拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作。
现在hotspot内置了此虚拟机
普通代码使用解释器;热点代码使用JIT编译器,然后缓存起来。
10.2、Exact VM
为了解决上一个虚拟机问题,jdk1.2时,sun提供了此虚拟机。
Exact Memory Management:准确式内存管理
- 也可以l叫Non-Conservative/Accurate Memory Management
- 虚拟机可以知道内存中某个位置的数据具体是什么类型。
具备现代高性能虚拟机的雏形
- 热点探测
- 编译器与解释器混合工作模式
只在Solaris平台短暂使用,其他平台上还是classic vm
- 英雄气短,终被Hotspot虚拟机替换
10.3、sun公司的Hotspot VM
HotSpot历史
- 最初由一家名为“Longview Technologies"的小公司设计
- 1997年,此公司被sun收购;2009年,Sun公司被甲骨文收购。
- JDK1,3时,HotSpot VM成为默认虚拟机
目前Hotspot占有绝对的市场地位,称霸武林。
- 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是HotSpot
- Sun/oracle JDK和OpenJDK的默认虚拟机
- 因此木课程中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的Gc机
制。(比如其他两个商用虚拟机都没有方法区的概念)
从服务器、桌面到移动端、嵌入式都有应用。
名称中的HotSpot指的就是它的热点代码探测技术。
- 通过计数器找到最具编译价值代码,触发即时编译或栈上替换
- 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡
10.4、BEA的Rockit
专注于服务器端应用
- 它可以不太关注程序启动速度,因此JRocki七内部不包含解析器实现,全部代码都靠即时编译器编译后执行。
大量的行业基准测试显示,JRockit JVM是世界上最快的JVM。
- 使用JRockit产品,客户己经体验到了显著的性能提高(一些超过了70号)和硬件成木的减少(达50号)。
优势:全面的Java运行时解决方案组合
- JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒级的M响应时间,适合财务、军事指挥、电信网络的需要
- MissionContro1服务套件,它是一组以极低的开销来监控、管理和分析生产
环境中的应用程序的工具。
2008年,BEA被0rac1e收购。
Oracle表达了整合两大优秀虚拟机的工作,大致在JDK8中完成。整合的方式是在HotSpot的基础上,移植JRock1t的优秀特性。
高斯林:目前就职于谷歌,研究人工智能和水下机器人
10.5、IBM的J9
全称:IBM Technology for Java virtual Machine,简称IT4J,内部代号:J9
市场定位与HotSpot:接近,服务器端、桌面应用、嵌入式等多用途VM
广泛用于IBM的各种Java产品。
目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机。
2017年左右,IBM发布了开源9VM,命名为OpenJ9,交给Eclipse基金
会管理,也称为Eclipse OpenJ9
10.6、KVM和CDC/CLDC Hotspot
Oracle在Java ME产品线上的两款虚拟机为:CDC/CLDC HotSpot Implementation VM
KVM(Kilobyte)是CLDC-HI早期产品
目前移动领域地位尴尬,智能手机被Android和ioS二分天下。
KVM简单、轻量、高度可移植,而向更低端的设备上还维持自己的一片市场
- 智能控制器、传感器
- 老人手机、经济欠发达地区的功能手机
所有的虚拟机的原则:一次编译,到处运行。
10.7、Azul VM
前面三大“高性能Java虚拟机”使用在通用硬件平台上
这里Azu1VM和BEA Liquid VM是与特定硬件平台绑定、软硬件配合的专有虚拟机
- 高性能Java虚拟机中的战斗机。
Azul VM是Azul Systems公司在HotSpot基础上进行大量改进,运行于Azul Systems公司的专有硬件vega系统上的Java虚拟机。
每个Azu1VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器、专有硬件优化的线程调度等优秀特性。
2010年,Azu1 Systems公司开始从硬件转向软件,发布了自己的zing JVM,可以在通用x86平台上提供接近于Vega系统的特性。
10.8、Liquid VM
高性能Java虚拟机中的战斗机。
BEA公司开发的,直接运行在自家Hypervisor系统上
Liquid VM即是现在的JRockit VE(virtual Edition),Liquid不需要操作系统的支持,或者说它自己本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等。
随着JRockit虚拟机终止开发,Liquid VM项目也停止了。
10.9、Apache Harmolhy
Apache也曾经推出过与JDK1.5和JDK1.6兼容的Java运行平台Apache Harmony
它是IBM和Intel联合开发的开源JVM,受到同样开源的openJDK的压制,Sun坚决不让Harmony?获得JCP认证,最终于2011年退役,IBM转而参与OpenJDK
虽然目前并没有Apache Harmony被大规模商用的案例,但是它的Java
类库代码吸纳进了Android SDK。
10.10、Microsoft JVM
微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM。
只能在window平台下运行。但确是当时windows下性能最好的Java VM。
1997年,Sun以侵犯商标、不正当竞争罪名指控微软成功,赔了sun很多
钱。微软在windowsXP SP3中抹掉了其VM。现在windows上安装的jdk
都是HotSpot。
10.11、 TaobaoJVM
由AliJVM材队发布。阿里,国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域,需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。
基于OpenJDK开发了自己的定制版木AlibabaJDK,简称AJDK。是整个阿里Java体系的基石。
基于OpenJDK HotSpot VM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机。
- 创新的GCIH(GC invisible heap)技术实现了off-heap,即将生命周期较长的Java对象从heap中移到heap之外,并且Gc不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
- GCIH中的对象还能够在多个Java虚拟机进程中实现共享
- 使用crc32指令实现JVM intrinsic降低JNI的调用开销
- PMU hardware的Java profiling tool和诊断协助功能
- 针对大数据场景的ZenGC
taobao vm应用在阿里产品上性能高,硬件严重依赖intel的cpu,损失了兼容性,但提高了性能
- 目前已经在淘宝、天猫上线,把Oracle官方JVM版本全部替换了。
10.12、Dalvik VM
谷歌开发的,应用于Android.系统,并在Android2.2中提供了JIT,发展迅猛。
Dalvik VM只能称作虚拟机,而不能称作“Java虚拟机”,它没有遵循Java
虚拟机规范
不能直接执行Java的class文件
基于寄存器架构,不是jvm的栈架构。
执行的是编译以后的dex(Dalvik Executable)文件。执行效率比较高。
- 它执行的dex(Dalvik Executable)文件可以通过class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。
Android5.0使用支持提前编译(Ahead of Time Compilation,AoT)的ART VM替换Dalvik VM.
10.13、其他JVM
具体VM的内存结构,其实取决于其实现不同厂商的VM,或者同一厂商发布的不同版本,都有可能存在一定差异。主要以Oracle HotSpot为默认虚拟机。
10.13、Graal VM
2018年4月,Oracle Labs公开了Graa1M,号称"Run Programs Faster Anywhere",勃勃野心。与1995年java的”write once,run anywhere"遥相呼应。
Graal VM在HotSpot VM基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用。语言包括:Java、Scala、Groovy、Kotlin;C、C++、JavaScript、Ruby、Python、R等
支持不同语言中混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件
工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中间表示。Graal VM提供Truffle工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化,获得比原生编译器更优秀的执行效率。
如果说HotSpot有一天真的被取代,Graal VM希望最大。但是Java的软件生态没有丝毫变化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rhghX3nJ-1682652254710)(https://gitee.com/lin-xugeng/image2/raw/master/img/202304192159186.png)]
二、类加载子系统
1、内存结构概述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XM1gd4gT-1682652254710)(https://gitee.com/lin-xugeng/image2/raw/master/img/202304192321223.png)]
如果自己手写一个Java虚拟机的话,主要考虑哪些结构呢?
类加载器和执行引擎
2、类加载器与类的加载过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmltdTRq-1682652254711)(https://gitee.com/lin-xugeng/image2/raw/master/img/202304200017703.png)]
类加载器子系统负责从文件系统或者网络中加载c1ass文件,class文件在文件开头有特定的文件标识。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
2.1、类加载器ClassLoader角色
1.class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这
个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
2.class fi1e加载到JVM中,被称为DNA元数据模板,放在方法区。
3.在.class文件->JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器class Loader),扮演一个快递员的角色
2.2、加载:
1.通过一个类的全限定名获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类
的各种数据的访问入口
补充:加载.class文件的方式
- 本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:SP应用
- 从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防class文件被反编译的保护措施
2.3、链接
验证(Verify):
-
目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
-
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备(Prepare):
-
为类变量分配内存并且设置该类变量的默认初始值,即零值。
package com.lxg.java;/*** @author shkstart* @create 2020 上午 11:43*/ public class HelloApp {private static int a = 1;//prepare:a = 0 ---> initial : a = 1public static void main(String[] args) {System.out.println(a);} }
-
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
-
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析(Resolve):
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着V在执行完初始化之后再执行:
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的C1ass文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等:对应常量池中的
CONSTANT_class_inEo、CONSTANT_Fieldref_info、CONSTANT Methodref info等。
可以查看字节码文件反编译和二进制文件的结果
2.4、初始化
初如化阶段就是执行类构造器方法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
构造器方法中指令按语句在源文件中出现的顺序执行。
()不同于类的构造器。(关联:构造器是虚拟机视角下的())
若该类具有父类,WM会保证子类的()执行前,父类的()已经执行完毕。
虚拟机必须保证一个类的()方法在多线程下被同步加锁。
package com.lxg.java;/*** @author shkstart* @create 2020 上午 11:23*/
public class DeadThreadTest {public static void main(String[] args) {Runnable r = () -> {System.out.println(Thread.currentThread().getName() + "开始");DeadThread dead = new DeadThread();System.out.println(Thread.currentThread().getName() + "结束");};Thread t1 = new Thread(r,"线程1");Thread t2 = new Thread(r,"线程2");t1.start();t2.start();}
}class DeadThread{static{if(true){System.out.println(Thread.currentThread().getName() + "初始化当前类");while(true){}}}
}
只会初始化一次,并且谁抢到了就会加锁
package com.lxg.java;/*** @author shkstart* @create 2020 下午 6:01*/
public class ClassInitTest {private static int num = 1;static{num = 2;number = 20;System.out.println(num);//System.out.println(number);//报错:非法的前向引用。}private static int number = 10; //linking之prepare: number = 0 --> initial: 20 --> 10public static void main(String[] args) {System.out.println(ClassInitTest.num);//2System.out.println(ClassInitTest.number);//10}
}
在类加载的过程中,首先会执行静态成员变量的初始化,包括静态变量和静态代码块。因此,在这段代码中,private static int number = 10;会先被执行,其值会被赋为0。然后,静态代码块会被执行,将num的值赋为2,number的值赋为20。最后,number的值又被赋为10,这是因为在类加载的linking阶段,number被赋为默认值0,而在initial阶段,又被重新赋值为20,最终又被赋值为10。因此,静态成员变量的初始化顺序是先执行静态变量的初始化,再执行静态代码块的内容。
Java非法向前引用变量_指尖凉的博客-CSDN博客
如果字段会进行初始化,一定要防止在初始化前程序中访问默认值(可以赋值但不可访问),这会导致很多意想不到的麻烦
静态代码的执行顺序:
静态代码块的执行顺序是在类被加载时执行,且只会执行一次。具体的执行顺序如下:1. 先执行父类的静态代码块,再执行子类的静态代码块。2. 静态代码块的执行顺序与它们在类中出现的顺序有关,按照代码顺序依次执行。3. 静态变量的初始化也是在静态代码块中进行的,因此静态变量的初始化顺序也与静态代码块的执行顺序有关。总之,静态代码块的执行顺序是在类加载时执行,按照代码顺序依次执行,且只会执行一次。
这里给出一个完整的例子,包括类的加载、初始化和静态变量的初始化等过程:
public class StaticBlockExample {static {System.out.println("StaticBlockExample静态代码块1");}public static int staticVar = 1;static {System.out.println("StaticBlockExample静态代码块2");}public static void main(String[] args) {System.out.println("StaticBlockExample静态变量的值为:" + staticVar);}
}class ChildStaticBlockExample extends StaticBlockExample {static {System.out.println("ChildStaticBlockExample静态代码块1");}public static int childStaticVar = 2;static {System.out.println("ChildStaticBlockExample静态代码块2");}
}
输出结果为:
StaticBlockExample静态代码块1
StaticBlockExample静态代码块2
ChildStaticBlockExample静态代码块1
ChildStaticBlockExample静态代码块2
StaticBlockExample静态变量的值为:1
我们可以看到,在程序运行时,首先加载StaticBlockExample
类,执行它的静态代码块,输出"StaticBlockExample静态代码块1"和"StaticBlockExample静态代码块2",然后初始化静态变量staticVar
的值为1。
接着,加载ChildStaticBlockExample
类,执行它的静态代码块,输出"ChildStaticBlockExample静态代码块1"和"ChildStaticBlockExample静态代码块2",然后初始化静态变量childStaticVar
的值为2。
最后,在主函数中输出静态变量staticVar
的值,输出结果为"StaticBlockExample静态变量的值为:1"。
3、类加载器分类
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)附自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类classLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
package com.lxg.java1;/*** @author shkstart* @create 2020 上午 9:22*/
public class ClassLoaderTest {public static void main(String[] args) {//获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//获取其上层:扩展类加载器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d//获取其上层:获取不到引导类加载器ClassLoader bootstrapClassLoader = extClassLoader.getParent();System.out.println(bootstrapClassLoader);//null//对于用户自定义类来说:默认使用系统类加载器进行加载ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。ClassLoader classLoader1 = String.class.getClassLoader();System.out.println(classLoader1);//null}
}
3.1、虚拟机自带的加载器
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、java、sun等开头的类
**扩展类加载器(**Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher:$ExtclassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.Launcher:$AppclassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemclassLoader()方法可以获取到该类加载器
package com.lxg.java1;/*package com.lxg.java1;import sun.security.ec.CurveDB;import java.net.URL;
import java.security.Provider;*/import sun.security.ec.ECKeyFactory;
import sun.security.util.CurveDB;import java.net.URL;
import java.security.Provider;/*** @author shkstart* @create 2020 上午 12:02*/
public class ClassLoaderTest1 {public static void main(String[] args) {System.out.println("**********启动类加载器**************");//获取BootstrapClassLoader能够加载的api的路径URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();for (URL element : urLs) {System.out.println(element.toExternalForm());}//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器ClassLoader classLoader = Provider.class.getClassLoader();System.out.println(classLoader);//nullSystem.out.println("***********扩展类加载器*************");String extDirs = System.getProperty("java.ext.dirs");for (String path : extDirs.split(";")) {System.out.println(path);}//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器ClassLoader classLoader1 = ECKeyFactory.class.getClassLoader();System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1b6d3586}
}
用户自定义类加载器
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
用户自定义类加载器实现步骤:
-
开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
-
在JDK1.2之前,在自定义类加载器时,总会去继承classLoader类并重写loadclass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
package com.lxg.java1;import java.io.FileNotFoundException;/*** 自定义用户类加载器* @author shkstart* @create 2019 下午 12:21*/ public class CustomClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] result = getClassFromCustomPath(name);if(result == null){throw new FileNotFoundException();}else{return defineClass(name,result,0,result.length);}} catch (FileNotFoundException e) {e.printStackTrace();}throw new ClassNotFoundException(name);}private byte[] getClassFromCustomPath(String name){//从自定义路径中加载指定类:细节略//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。return null;}public static void main(String[] args) {CustomClassLoader customClassLoader = new CustomClassLoader();try {Class<?> clazz = Class.forName("One",true,customClassLoader);Object obj = clazz.newInstance();System.out.println(obj.getClass().getClassLoader());} catch (Exception e) {e.printStackTrace();}} }
-
在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
4、关于ClassLoader
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
sun.misc.Lauhcher它是一个java虚拟机的入口应用。
4.1、获取ClassLoader的途径
方式一:获取当前类的ClassLoader
clazz.getClassLoader()
方式二:获取当前线程上下文的Class工pader
Thread.currentThread().getContextclassLoader ()
方式三:获取系统的ClassLoader
ClassLoader.getsystemClassLoader()
方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader ()
package com.lxg.java1;/*** @author shkstart* @create 2020 上午 10:59*/
public class ClassLoaderTest2 {public static void main(String[] args) {try {//1.ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();System.out.println(classLoader);//2.ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();System.out.println(classLoader1);//3.ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();System.out.println(classLoader2);} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
5、双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
5.1、工作原理
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终将到达顶层的启动类加载器
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
5.2、案例1
package com.lxg.java1;/*** @author shkstart* @create 2020 上午 11:39*/
public class StringTest {public static void main(String[] args) {String str = new String();System.out.println("hello,atguigu.com");StringTest test = new StringTest();System.out.println(test.getClass().getClassLoader());}
}
5.3、案例2
优势
避免类的重复助加载
保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.Shkstart
java.lang.SecurityException:Prohibited package name:java.lang
5.4、沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.clss),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
6、其他
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包活包名。
- 加载这个类的classLoader(指ClassLoader实例对象)必须相同
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的classLoader实例对象不同,那么这两个类对象也是不相等的。
6.1、对类加载器的引用
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
6.2、类的主动使用和被动使用
Java程序对类的使用方式分为:主动使用和被动使用。
-
主动使用,又分为七种情况:
-
创建类的实例
-
访问某个类或接口的静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射(比如:Class.forName(“com.lxg.test”))
-
初始化一个类的子类
-
Java虚拟机启动时被标明为启动类的类
-
JDK7开始提供的动态语言支持:
java.lang.invoke.MethodHandle实例的解析结果
REF_getStatic、REF_ putstatic、REF_ invokeStatic句柄为应的类没有初始化,则初始化
-
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
三、运行时数据区概述及线程
1、运行时数据库内部结构
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局。
Java虚拟机定义了若千种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。灰色的为单独线程私有的,红色的为多个线程共享的。即:
- 每个线程:独立包括程序计数器、栈、本地栈
- 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
2、线程
线程是一个程序里的运行单元。JM允许一个应用有多个线程并行的执行。
在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
- 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本
地线程初始化成功,它就会调用Java线程中的run()方法。
2.1、JVM系统线程
如果你使用console或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己创建的线程。
这些主要的后台系统线程在Hotspot JVM.里主要是以下几个:
- 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
- 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
- GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
- 编译线程:这种线程在运行时会将字节码编译成到本地代码。
- 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进
行处理。
四、程序计数器(PC寄存器)
1、PC Register介绍
https://docs.oracle.com/javase/specs/jvms/se8/html/
JVM中的程序计数寄存器(Program CounterRegister)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
作用:
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned)
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
它是唯一一个在Java虚拟机规范中没有规定任何OutofMemoryError情况的区域。
GC即垃圾回收(Garbage Collection)
2、举例说明
3、两个常见问题
3.1、
使用PC寄存器存储字节码指令地址有什么用呢?
为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
3.2、PC寄存器为什么会被设定为线程私有?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况
由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
3.3、CPU时间片
CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
五、虚拟机栈
1、虚拟机栈概述
1.1、虚拟机栈出现的背景
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
1.2、初步印象
有不少Java开发人员-I提到Java内存结构,就会非常粗粒度地将JVM中的内存区理解为仅有Java堆(heap)和Java栈(stack)?为什么?
1.3、内存中的栈与堆
栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
1.4、虚拟机栈基本内容
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
- 是线程私有的
生命周期
生命周期和线程一致。
作用
主管Java程序的运行,它保存方法的局部变量(对象的引用地址、8种基本数据类型)、部分结果,并参与方法的调用和返回。
- 局部变量 VS 成员变量)或属性)
- 基本数据变量 VS 引用类型变量(类、数组、接口)
package com.lxg.java;/*** @author shkstart* @create 2020 下午 8:32*/
public class StackTest {public static void main(String[] args) {StackTest test = new StackTest();test.methodA();}public void methodA() {int i = 10;int j = 20;methodB();}public void methodB(){int k = 30;int m = 40;}
}
栈的特点(优点)
-
栈是一种快速有放的分配存储方式,访问速度仅次于程序计数器。
-
JVM直接对Java栈的操作只有两个:
-
每个方法执行,伴随着进栈(入栈、压栈)
-
执行结束后的出栈工作
-
-
对于栈来说不存在垃圾回收问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPtm83Mx-1682652254719)(https://gitee.com/lin-xugeng/image2/raw/master/img/202304230033244.png)]
不存在GC、但存在OOM异常
面试题:开发中遇到的异常有哪些?
栈中可能出现的异常
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
-
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowErrOr异常。
package com.lxg.java;/*** 演示栈中的异常:StackOverflowError* @author shkstart* @create 2020 下午 9:08** 默认情况下:count : 11420* 设置栈的大小: -Xss256k : count : 2465*/ public class StackErrorTest {private static int count = 1;public static void main(String[] args) {System.out.println(count);count++;main(args);}}
-
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutofMemoryError异常。
package com.lxg.java;import java.util.ArrayList; import java.util.List;public class OOMDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();while (true) {list.add("Hello World");}} }
怎么设置虚拟机栈的大小
Java 虚拟机栈(Java Virtual Machine Stack)用于存储方法调用和局部变量等信息,是 JVM 中的一部分。虚拟机栈的大小可以通过启动参数 -Xss
来设置。
默认情况下,虚拟机栈的大小为 1MB。可以使用以下命令设置虚拟机栈的大小为 2MB:
java -Xss2m MyClass
需要注意的是,虚拟机栈的大小受到操作系统的限制,如果设置的虚拟机栈大小超过了操作系统的限制,将会导致程序无法正常运行。因此,在设置虚拟机栈大小时,需要根据实际情况进行调整。
虚拟机内存
Java 虚拟机在启动时会根据系统的物理内存自动分配一定大小的内存空间,这个大小通常称为初始堆大小(Initial Heap Size)。
默认情况下,Java 虚拟机的初始堆大小为物理内存的 1/64,最小值为 1MB,最大值为物理内存的 1/4。例如,如果你的电脑物理内存为 8GB,那么 Java 虚拟机的初始堆大小为 128MB,最小值为 1MB,最大值为 2GB。
需要注意的是,初始堆大小只是 Java 虚拟机的一个默认值,实际应用中需要根据具体情况进行调整,以满足应用程序的内存需求。可以使用启动参数 -Xms
和 -Xmx
分别设置 Java 虚拟机的初始堆大小和最大堆大小。例如,使用以下命令设置初始堆大小为 256MB,最大堆大小为 512MB:
java -Xms256m -Xmx512m MyClass
永久代和元空间和方法区有区别吗
永久代和元空间都是Java虚拟机用于存储类信息的区域,但是它们是在不同的Java虚拟机版本中使用的。在Java 7及之前的版本中,Java虚拟机使用永久代来存储类信息,而在Java 8及之后的版本中,Java虚拟机使用元空间来存储类信息。因此,永久代和元空间是Java虚拟机的两种不同的实现方式。
方法区是Java虚拟机规范中定义的一个概念,它是Java虚拟机用于存储类信息的区域,包括类的代码、静态变量、常量池等。在Java 7及之前的版本中,永久代就是方法区的一种实现方式。而在Java 8及之后的版本中,元空间也是方法区的一种实现方式。因此,方法区可以使用永久代或元空间来实现,但它们都属于Java虚拟机中用于存储类信息的区域。
2、栈的存储单位
本地方法栈和虚拟机栈是Java虚拟机(JVM)中的两个重要的栈结构。
虚拟机栈是用于执行Java方法的栈,每个线程都有自己的虚拟机栈,用于存储局部变量、方法参数、方法返回值和操作数栈等信息。虚拟机栈的大小可以通过-Xss参数进行设置,如果线程请求的栈深度超过了虚拟机栈的最大深度,就会抛出StackOverflowError异常。
本地方法栈是用于执行native方法的栈,与虚拟机栈类似,每个线程都有自己的本地方法栈。本地方法栈中存储的是native方法的信息,包括参数、返回值和局部变量等。如果本地方法请求的栈深度超过了本地方法栈的最大深度,就会抛出StackOverflowError异常。
虚拟机栈和本地方法栈的区别在于,虚拟机栈是用于执行Java方法的栈,而本地方法栈是用于执行native方法的栈。虚拟机栈和本地方法栈的大小都可以通过JVM参数进行设置,但是两者的最大深度是独立的,互相不受影响。
2.1、栈中存储什么?
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
复习:
OOP:
OOP(Object-Oriented Programming,面向对象编程)是一种编程范式,它将程序中的数据和操作数据的方法组合成一个对象,以此来描述现实世界中的事物及其关系。
理解OOP可以从以下几个方面入手:
-
对象:OOP将程序中的实体看作是对象,这些对象具有属性和方法。对象是现实世界中的实体在程序中的抽象,可以是人、车、动物、商品等。
-
封装:OOP中的封装是指将数据和方法封装在对象中,使得对象的内部状态不被外部直接访问和修改。外部只能通过对象提供的接口来访问和修改对象的内部状态,从而保证数据的安全性和一致性。
-
继承:OOP中的继承是指可以通过一个已存在的类来创建一个新的类,新类继承了原有类的属性和方法,并可以在此基础上进行扩展和修改。继承可以减少代码重复,提高代码的可维护性和可复用性。
-
多态:OOP中的多态是指同一种操作可以作用于不同的对象,产生不同的结果。多态可以提高代码的灵活性和可扩展性,使得程序更加易于扩展和维护。
通过这些方面的理解,可以更好地理解OOP的核心思想和特点,以及OOP在软件开发中的优势和应用。
2.2、栈运行原理
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
package com.lxg.java1;/*** @author shkstart* @create 2020 下午 4:11** 方法的结束方式分为两种:① 正常结束,以return为代表 ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束**/
public class StackFrameTest {public static void main(String[] args) {try {StackFrameTest test = new StackFrameTest();test.method1();} catch (Exception e) {e.printStackTrace();}System.out.println("main()正常结束");}public void method1(){System.out.println("method1()开始执行...");method2();System.out.println("method1()执行结束...");
// System.out.println(10 / 0);// return ;//可以省略
}public int method2() {System.out.println("method2()开始执行...");int i = 10;int m = (int) method3();System.out.println("method2()即将结束...");return i + m;}public double method3() {System.out.println("method3()开始执行...");double j = 20.0;System.out.println("method3()即将结束...");return j;}}
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
package com.lxg.java1;/*** @author shkstart* @create 2020 下午 4:11** 方法的结束方式分为两种:① 正常结束,以return为代表 ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束**/
public class StackFrameTest {public static void main(String[] args) {try {StackFrameTest test = new StackFrameTest();test.method1();} catch (Exception e) {e.printStackTrace();}System.out.println("main()正常结束");}public void method1(){System.out.println("method1()开始执行...");method2();System.out.println("method1()执行结束...");System.out.println(10 / 0);// return ;//可以省略
}public int method2() {System.out.println("method2()开始执行...");int i = 10;int m = (int) method3();System.out.println("method2()即将结束...");return i + m;}public double method3() {System.out.println("method3()开始执行...");double j = 20.0;System.out.println("method3()即将结束...");return j;}}
2.3、栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(operand stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
3、局部变量表
局部变量表出被称之为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其核套调用次数就会减少。
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
3.1、关于Slot的理解
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
局部变量表,最基本的存储单元是S1ot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型
(reference),returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
-
byte、short、char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
-
long和double则占据两个Slot。
JVM会为局部变量表中的每一个Slot都分配一个访问素引,通过这个索引即可成功访
问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问了long或double类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
3.2、Slot的重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
package com.lxg.java;/*** @author shkstart* @create 2020 下午 10:24*/
public class SlotTest {public void localVarl() {int a = 0;System.out.println(a);int b = 0;}public void localVar2() {{int a = 0;System.out.println(a);}//此时的b就会复用a的槽位int b = 0;}
}
3.3、举例:静态变量与局部变量的对比
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋子程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦
定义了局部变量则必须人为的初始化,否则无法使用。
public void test5Temp(){int num;//System.out.println(num);//错误信息:变量num未进行初始化
}
这样的代码是错误的,没有赋值不能够使用
3.4、补充说明
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
4、操作数栈(Operand Stack)
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
- 比如:执行复制、交换、求和等操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nPn8Ls82-1682652254721)(https://gitee.com/lin-xugeng/image2/raw/master/img/202304232007696.png)]
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
栈中的任何一个元素都是可以任意的Java数据类型。
-
32bit的类型占用一个栈单位深度
-
64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
另外,我们说ava虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
5、代码追踪
/*
程序员面试过程中, 常见的i++和++i 的区别,放到字节码篇章时再介绍。*/
public void add(){//第1类问题:int i1 = 10;i1++;int i2 = 10;++i2;//两者无区别//第2类问题:int i3 = 10;int i4 = i3++;int i5 = 10;int i6 = ++i5;//i4=10,i3=11//i5=11,i6=11//第3类问题:int i7 = 10;i7 = i7++;int i8 = 10;i8 = ++i8;//i7=10//i8=11//第4类问题:int i9 = 10;int i10 = i9++ + ++i9;
}
6、栈顶缓存(Top-of-Stack Cashing)技术
前面提过,基于下栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,Hotspot JVM的设计者们提出了栈顶缓存(ToS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
7、动态链接(Dynamic Linking)
7.1、动态链接(或指向运行时常量池的方法引用)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
8、方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接:
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竞是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际
的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
package com.lxg.java2;/*** 说明早期绑定和晚期绑定的例子* @author shkstart* @create 2020 上午 11:59*/
class Animal{public void eat(){System.out.println("动物进食");}
}
interface Huntable{void hunt();
}
class Dog extends Animal implements Huntable{@Overridepublic void eat() {System.out.println("狗吃骨头");}@Overridepublic void hunt() {System.out.println("捕食耗子,多管闲事");}
}class Cat extends Animal implements Huntable{public Cat(){super();//表现为:早期绑定}public Cat(String name){this();//表现为:早期绑定}@Overridepublic void eat() {super.eat();//表现为:早期绑定System.out.println("猫吃鱼");}@Overridepublic void hunt() {System.out.println("捕食耗子,天经地义");}
}
public class AnimalTest {public void showAnimal(Animal animal){animal.eat();//表现为:晚期绑定}public void showHunt(Huntable h){h.hunt();//表现为:晚期绑定}
}
随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于c++语言中的虚函数(c++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
8.1、虚方法和非虚方法
非虚方法:
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
- 其他方法称为虚方法。
虚拟机中提供了以下几条方法调用指令:
普通调用指令:
1.invokestatic:调用静态方法,解析阶段确定唯一方法版本
2.invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法
动态调用指令:
5.invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
package com.lxg.java2;/*** 解析调用中非虚方法、虚方法的测试** invokestatic指令和invokespecial指令调用的方法称为非虚方法* @author shkstart* @create 2020 下午 12:07*/
class Father {public Father() {System.out.println("father的构造器");}public static void showStatic(String str) {System.out.println("father " + str);}public final void showFinal() {System.out.println("father show final");}public void showCommon() {System.out.println("father 普通方法");}
}public class Son extends Father {public Son() {//invokespecialsuper();}public Son(int age) {//invokespecialthis();}//不是重写的父类的静态方法,因为静态方法不能被重写!public static void showStatic(String str) {System.out.println("son " + str);}private void showPrivate(String str) {System.out.println("son private " + str);}public void show() {//invokestaticshowStatic("atguigu.com");//invokestaticsuper.showStatic("good!");//invokespecialshowPrivate("hello!");//invokespecialsuper.showCommon();//invokevirtualshowFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。//虚方法如下://invokevirtualshowCommon();info();MethodInterface in = null;//invokeinterfacein.methodA();}public void info(){}public void display(Father f){f.showCommon();}public static void main(String[] args) {Son so = new Son();so.show();}
}interface MethodInterface{void methodA();
}
8.2、关于invokeddynamic指令
JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现「动态类型语言」支持而做的一种改进。
但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对ava语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
这句话的意思是,静态类型语言在编译时就会确定变量的数据类型,而且变量的类型信息是固定的,无法改变。而动态类型语言在运行时才会确定变量的数据类型,变量的类型信息是根据变量的值来确定的,可以随时改变。因此,静态类型语言需要在编译时进行类型检查,而动态类型语言则可以在运行时进行类型检查。
package com.lxg.java2;/*** 体会invokedynamic指令* @author shkstart* @create 2020 下午 3:09*/
@FunctionalInterface
interface Func {public boolean func(String str);
}public class Lambda {public void lambda(Func func) {return;}public static void main(String[] args) {Lambda lambda = new Lambda();Func func = s -> {return true;};lambda.lambda(func);lambda.lambda(s -> {return true;});}
}
8.3、方法重写的本质
Java语言中方法重写的本质:
1.找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
2.如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回
java.1ang.IllegalAccessError异常。
3.否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜素和验证过程。
4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError.异常。
IllegalAccessError介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
8.4、虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目际的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
那么虚方法表什么时候被创建?
- 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
例子1:
例子2:
可卡犬
package com.lxg.java3;/*** 虚方法表的举例** @author shkstart* @create 2020 下午 1:11*/
interface Friendly {void sayHello();void sayGoodbye();
}
class Dog {public void sayHello() {}public String toString() {return "Dog";}
}
class Cat implements Friendly {public void eat() {}public void sayHello() {}public void sayGoodbye() {}protected void finalize() {}public String toString(){return "Cat";}
}class CockerSpaniel extends Dog implements Friendly {public void sayHello() {super.sayHello();}public void sayGoodbye() {}
}public class VirtualMethodTable {
}
dog虚方法表:
CockerSpaniel虚方法表:
cat虚方法表:
9、方法返回地址(Return Address)
存放调用该方法的pc寄存器的值。
一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的c计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
PC寄存器存储下一条指令的地址,当有一个方法结束运行时,此时要执行下一个方法的指令,就需要把PC寄存器里的值作为这个方法的返回值返回。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
当一个方法开始执行后,只有两种方式可以退出这个方法:
1、执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
-
一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
-
在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
package com.lxg.java3;import java.io.FileReader;
import java.io.IOException;
import java.util.Date;/**** 返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、* lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、* 实例初始化方法、类和接口的初始化方法使用。** @author shkstart* @create 2020 下午 4:05*/
public class ReturnAddressTest {public boolean methodBoolean() {return false;}public byte methodByte() {return 0;}public short methodShort() {return 0;}public char methodChar() {return 'a';}public int methodInt() {return 0;}public long methodLong() {return 0L;}public float methodFloat() {return 0.0f;}public double methodDouble() {return 0.0;}public String methodString() {return null;}public Date methodDate() {return null;}public void methodVoid() {}static {int i = 10;}//public void method2() {methodVoid();try {method1();} catch (IOException e) {e.printStackTrace();}}public void method1() throws IOException {FileReader fis = new FileReader("atguigu.txt");char[] cBuffer = new char[1024];int len;while ((len = fis.read(cBuffer)) != -1) {String str = new String(cBuffer, 0, len);System.out.println(str);}fis.close();}}
2、在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发
生异常的时候找到处理异常的代码。
10、一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
11、栈的相关面试题
1、举例栈溢出的情况?
StackOverflowError
通过-Xss设置栈的大小
递归调用方法
chatgpt:栈溢出是指程序在使用栈空间时,向栈空间写入数据时超出了栈空间的大小,导致数据覆盖了栈空间之外的内存区域。以下是一些可能导致栈溢出的情况:1. 递归调用过深,导致栈空间被耗尽。
2. 函数中定义了过多的局部变量,导致栈空间不足。
3. 函数中定义了过大的局部变量或数组,导致栈空间不足。
4. 函数中使用了过多的参数,导致栈空间不足。
5. 函数返回值过大,导致栈空间不足。
6. 函数调用时未正确保存现场,导致返回地址等数据被覆盖。这些情况都可能导致栈溢出,造成程序崩溃或安全漏洞。因此,在编写程序时应当注意避免这些情况的发生。
2、调整栈大小,就能保证不出现溢出吗?
不能保证
chatgpt调整栈大小可以增加栈空间的容量,从而减少栈溢出的风险,但不能完全保证不出现溢出。因为在程序运行过程中,栈空间的大小是动态变化的,而且栈空间的大小不仅受到程序本身的影响,还受到操作系统和硬件等因素的影响。因此,调整栈大小只是减少栈溢出的一种方式,还需要从程序设计和编写的角度出发,避免出现栈溢出的情况。在编写程序时,应当注意以下几点:1. 减少递归调用的深度,避免栈空间被耗尽。
2. 合理使用局部变量和数组,避免定义过多或过大的变量。
3. 减少函数参数的数量和大小,避免占用过多的栈空间。
4. 对于需要返回大量数据的函数,可以使用堆空间或全局变量来存储数据。
5. 在函数调用时,正确保存现场,避免返回地址等数据被覆盖。通过以上措施,可以有效地避免栈溢出的发生。
3、分配的栈内存越大越好吗?
不是
栈内存越大线程数就会越少,因为物理内存大小是固定的,栈内存越大,可运行线程就越少。比如,一个线程使用栈内存,假设使用1M的内存,物理内存500M,理论上就可以有500个线程同时运行。如果每个线程设置2M,那么只能同时有250个线程运行。所以栈内存分配越大并不是越好,它分配大了通常只是能够进行多次的方法递归调用,而不会增快程序的运行效率,反而会影响线程数目的变少。一般采用默认的就可以,不必在程序启动的时候手动修改。
内存浪费:分配过大的栈内存也会导致内存浪费,因为栈内存是在编译时就分配的,如果分配的栈内存过大,可能会导致未使用的内存浪费。
4、垃圾回收是否会涉及到虚拟机栈?
不会
方法区和堆:有Error和GC
本地方法栈和虚拟机栈:有Error没有GC
程序计数器:没有Error也没有GC
因为栈内存在方法调用结束后都会自动弹出栈。
垃圾回收不会涉及到虚拟机栈。虚拟机栈是用于存储方法调用和局部变量的内存区域,它与堆、方法区一样,是Java虚拟机运行时数据区域之一。垃圾回收主要是对堆内存进行回收,即回收不再被引用的对象。虚拟机栈中的对象随着方法的结束而自动释放,不需要进行垃圾回收。当一个方法被调用时,Java虚拟机会在虚拟机栈中为该方法创建一个栈帧,用于存储该方法的局部变量、操作数栈、方法返回值等信息。当方法执行完毕后,该栈帧会被弹出,栈中的数据会被销毁,不会对垃圾回收造成影响。因此,虚拟机栈和垃圾回收是两个独立的概念,它们之间没有直接的关联。
5、方法中定义的局部变量是否线程安全?
方法中定义的局部变量一般是线程安全的,因为它们只在方法内部使用,并且每次方法调用都会创建一个新的局部变量。因此,不同的线程可以同时调用该方法而不会相互影响。然而,如果方法中的局部变量是可变对象,而且多个线程同时修改了该对象,那么就会存在线程安全问题。在这种情况下,需要使用同步机制来保证线程安全,例如使用锁或者使用线程安全的数据结构。
ackTrace();
}
}
public void method1() throws IOException {FileReader fis = new FileReader("atguigu.txt");char[] cBuffer = new char[1024];int len;while ((len = fis.read(cBuffer)) != -1) {String str = new String(cBuffer, 0, len);System.out.println(str);}fis.close();
}
}
2、在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发
生异常的时候找到处理异常的代码。[外链图片转存中...(img-cWPCT46j-1682652254725)][外链图片转存中...(img-4rJJvpwO-1682652254725)]### 10、一些附加信息栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。### 11、栈的相关面试题#### 1、举例栈溢出的情况?StackOverflowError通过-Xss设置栈的大小递归调用方法
chatgpt:
栈溢出是指程序在使用栈空间时,向栈空间写入数据时超出了栈空间的大小,导致数据覆盖了栈空间之外的内存区域。以下是一些可能导致栈溢出的情况:
- 递归调用过深,导致栈空间被耗尽。
- 函数中定义了过多的局部变量,导致栈空间不足。
- 函数中定义了过大的局部变量或数组,导致栈空间不足。
- 函数中使用了过多的参数,导致栈空间不足。
- 函数返回值过大,导致栈空间不足。
- 函数调用时未正确保存现场,导致返回地址等数据被覆盖。
这些情况都可能导致栈溢出,造成程序崩溃或安全漏洞。因此,在编写程序时应当注意避免这些情况的发生。
#### 2、调整栈大小,就能保证不出现溢出吗?不能保证
chatgpt
调整栈大小可以增加栈空间的容量,从而减少栈溢出的风险,但不能完全保证不出现溢出。因为在程序运行过程中,栈空间的大小是动态变化的,而且栈空间的大小不仅受到程序本身的影响,还受到操作系统和硬件等因素的影响。因此,调整栈大小只是减少栈溢出的一种方式,还需要从程序设计和编写的角度出发,避免出现栈溢出的情况。
在编写程序时,应当注意以下几点:
- 减少递归调用的深度,避免栈空间被耗尽。
- 合理使用局部变量和数组,避免定义过多或过大的变量。
- 减少函数参数的数量和大小,避免占用过多的栈空间。
- 对于需要返回大量数据的函数,可以使用堆空间或全局变量来存储数据。
- 在函数调用时,正确保存现场,避免返回地址等数据被覆盖。
通过以上措施,可以有效地避免栈溢出的发生。
#### 3、分配的栈内存越大越好吗?不是栈内存越大线程数就会越少,因为物理内存大小是固定的,栈内存越大,可运行线程就越少。比如,一个线程使用栈内存,假设使用1M的内存,物理内存500M,理论上就可以有500个线程同时运行。如果每个线程设置2M,那么只能同时有250个线程运行。所以栈内存分配越大并不是越好,它分配大了通常只是能够进行多次的方法递归调用,而不会增快程序的运行效率,反而会影响线程数目的变少。一般采用默认的就可以,不必在程序启动的时候手动修改。内存浪费:分配过大的栈内存也会导致内存浪费,因为栈内存是在编译时就分配的,如果分配的栈内存过大,可能会导致未使用的内存浪费。#### 4、垃圾回收是否会涉及到虚拟机栈?不会方法区和堆:有Error和GC本地方法栈和虚拟机栈:有Error没有GC程序计数器:没有Error也没有GC**因为栈内存在方法调用结束后都会自动弹出栈。**
垃圾回收不会涉及到虚拟机栈。
虚拟机栈是用于存储方法调用和局部变量的内存区域,它与堆、方法区一样,是Java虚拟机运行时数据区域之一。垃圾回收主要是对堆内存进行回收,即回收不再被引用的对象。虚拟机栈中的对象随着方法的结束而自动释放,不需要进行垃圾回收。
当一个方法被调用时,Java虚拟机会在虚拟机栈中为该方法创建一个栈帧,用于存储该方法的局部变量、操作数栈、方法返回值等信息。当方法执行完毕后,该栈帧会被弹出,栈中的数据会被销毁,不会对垃圾回收造成影响。
因此,虚拟机栈和垃圾回收是两个独立的概念,它们之间没有直接的关联。
#### 5、方法中定义的局部变量是否线程安全?
方法中定义的局部变量一般是线程安全的,因为它们只在方法内部使用,并且每次方法调用都会创建一个新的局部变量。因此,不同的线程可以同时调用该方法而不会相互影响。
然而,如果方法中的局部变量是可变对象,而且多个线程同时修改了该对象,那么就会存在线程安全问题。在这种情况下,需要使用同步机制来保证线程安全,例如使用锁或者使用线程安全的数据结构。