• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
在赶路的我
博客园    首页    新随笔    联系   管理    订阅  订阅

JVM(图片丢失)

JVM学习

JVM的体系结构

  • JVM包括java的堆(heap),栈(stack),方法区(method area),本地方法区(native),程序计数器,本地方法栈,本地方法库,执行引擎

  • JRE中包括JVM

  • 详细的JVM图

类加载器

  • java实例的创建过程

类加载器总共有4类

  1. 应用程序加载器
  2. 扩展加载器
  3. 主程序加载器(根加载器)
  4. JVM自带的加载器

双亲委派机制

  • 作用:为了防止恶意在程序中重写JRE中的一些类,导致一些bug
  • 例子: 如果写了与JRE中自带的类的相同的类,包名一样,则不会执行自己写的,而是执行自带的
  • 执行机制

了解Class

  • 测试
B b = new B();
Class<? extends B> bClass = b.getClass();
System.out.println(bClass.getClassLoader());
System.out.println(bClass.getClassLoader().getParent());
//对于根加载器获取不到
System.out.println(bClass.getClassLoader().getParent().getParent());
  • 结果
//应用程序类加载器
jdk.internal.loader.ClassLoaders$AppClassLoader@7a4f0f29
//
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1c4af82c
null
class demo1.B

借鉴别人的博客

JAVA类加载器包括几种?
引导类加载器 bootstrap class loader
  启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器 extensions class loader
  它负责加载JAVA_HOME/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

应用程序类加载器 application class loader
  应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

  • 关于classpath路径

自定义类加载器 java.lang.classloder
  就是自定义啦,通过继承java.lang.ClassLoader类的方式

类加载器之间的关系
  启动类加载器,由C++实现,没有父类。
  拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
  系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
  自定义类加载器,父类加载器肯定为AppClassLoader。

双亲委派机制
  请注意双亲委派模式中的父子关系并非通常所说的类继承关系。
  其工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。

双亲委派机制作用
  通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改

如何自定义一个类加载器?
  通过继承ClassLoad定义一个类加载器。

自定义加载器应用场景
  如Tomcat容器,每个WebApp有自己的ClassLoader,加载每个WebApp的ClassPath路径上的类,一旦遇到Tomcat自带的Jar包就委托给CommonClassLoader加载。同包的隔离。另外成熟的开源框架,都有自己的classloade。

native(本地方法)

  • java在类中存在使用native定义的方法接口

native的作用

  • 凡是标注了native关键字的,说明java的作用范围已经达不到了,只能使用本地方法来调用c语言的库

本地方法的作用

  • 用于扩展java的使用,融合不同的编程语言为java所用,java诞生之初,c和c++热门,如果想要立足,必须融合c语言
  • 于是java在内存中专门开辟了一块区域本地方法栈用于登记本地方法,在最终执行的时候调用本地方法库中的方法

在哪里使用本地方法

  • 在java调用系统资源的时候,如java程序操作打印机等,但是在企业级应用中很少用到。

pc寄存器

方法区

  • 方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简答来说,所有定义的方法的信息都保存在该区域,属于共享区间
  • 静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
  • static,final,Class,常量池

深入理解栈

前提

  • 好的程序=数据结构+算法
  • 现阶段:我的程序=框架+业务逻辑(没有一点技术含量,容易被淘汰)

Java发展的重大事件

  1. 1990年,在sun计算机公司中,由Patrick Naughton,MikeSheridan及James Gosling领导的小组Green Team,开发出的新的程序语言,命名为Oak,后期改名为Java
  2. 1995年,Sun正式发布Java和HotJava产品,Java首次公开亮相
  3. 1996年1月23日Sun Microsystems发布了JDK1.0
  4. 1998年,JDK1.2版本发布。同时,Sun发不了JSP/Servlet,EJB规范,以及将Java分成了J2EE,J2SE,J2ME。这表明了Java开始向企业,桌面应用和移动设备应用3大领域挺进
  5. 2000年,JDK1.3发布,Java HotSpot Virtual Machine正式发布,成为Java的默认虚拟机
  6. 2002年,JDK1.4发布,古老的Classic虚拟机退出历史舞台
  7. 2003年年底,Java平台的Scala正式发布,同年Groovy也加入了Java阵营
  8. 2004年,JDK1.5发布,同时JDK1.5改名为JavaSE5.0
  9. 2006年,JDK6发布,同年,Java开源并建立了OpenJDK。顺理成章,Hotspot虚拟机也成为了OpenJDK中的默认虚拟机。
  10. 2007年,Java平台迎来了新伙伴Clojure
  11. 2008年,Oracle收购了BEA,得到了JRockit虚拟机
  12. 2009年,Twitter宣布把后台大部分程序从Ruby迁移到Scala,这是Java平台的又一次大规模应用
  13. 2010年,Oracle收购了Sun,获得Java商标和最具价值的HotSpot虚拟机,此时Oracle拥有市场占有率最高的两款虚拟机Hotspot和JRockit,并计划在未来将他们整合:HotRockit。JCP组织管理Java语言
  14. 2011年,JDK7发布,在JDK1.7u4中,正式启用了新的垃圾回收器G1
  15. 2017年,JDK9发布。将G1设置为默认的GC,替代CMS,同年,IBM的J9发布,形成了现在的Open J9社区
  16. 2018年,Android的Java侵权案判决,Google赔偿Oracle88亿美元
  17. 同年Oracle宣布JavaEE成为历史名词,JDBC,JMS,Servlet赠与Eclipse基金会
  18. 同年,JDK11发布,LTS版本的JDK,发布革命性的ZGC,调整JDK授权许可
  19. 2019年,JDK12发布,加入RedHat领导开发的Shenandoah GC

虚拟机与虚拟机介绍

  • 所谓虚拟机,就是一台虚拟的计算机,它是一款软件,用来执行一些列虚拟计算机指令,大体上,虚拟机可以分为系统虚拟机和程序虚拟机
    • VMware就属于系统虚拟机,他们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台
    • Java虚拟机就是程序虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令为Java字节码指令
  • 无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中

Java虚拟机

  • java虚拟机是一台执行java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必有java语言编译而成
  • JVM平台的各种语言可以共享java虚拟机带来的跨平台性,优秀的垃圾回收器,以及可靠的即时编译器
  • Java技术的核心就是Java虚拟机,因为所有的Java程序都运行在Java虚拟机内部
  • java虚拟机的原材料就是字节码
  • 作用:
    • java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪儿
    • 特点:
      • 一次编译,处处运行
      • 自动内存管理
      • 自动垃圾回收功能

JVM的整体结构

  • 方法区和堆区是线程所共享的
  • 虚拟机栈(java栈)和本地方法栈和程序计数器(pc寄存器)是每一个线程独份的

  • 执行引擎包含解释器和JIT即时编译器和垃圾回收器
    • 执行引擎是将字节码文件转换为机器指令来运行

  • 解释器是用来将字节码文件转换为机器指令来执行的
  • JIT编译器是用来将重复用到的代码的字节码转换成机器指令并且缓存起来,以供下一次使用,是为了提高效率的
  • 现在主流的虚拟机的执行引擎都是采用解释器和JIT即时编译器并行使用的

区分栈的指令集架构和寄存器的指令集架构

  • Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构

栈指令集架构和寄存器指令集架构的区别

  • 栈式指令集架构
    1. 设计和实现更简单,适用于资源受限的系统
    2. 避开了寄存器的分配难题:使用零地址指令方式分配,就是没有地址,因为只对栈顶元素进行操作
    3. 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现
    4. 不需要硬件支持,可移植性更好,更好实现跨平台
  • 寄存器指令集架构
    1. 典型的应用为x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机
    2. 指令集架构则完全依赖硬件,可移植性差
    3. 性能优秀,执行更高效
    4. 花费更少的指令去完成一项操作
    5. 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令,二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主

Java反编译字节码文件查看指令集

  • 使用javap -v -p Demo.class

  • 示例:

    • public class Demo1 {
          public static void main(String[] args) {
             int i=1;
             int j=2;
             int sum = j+i;
          }
      }
      
    • 该类需要执行的指令集为

      • iconst_1:代表引入常数1
      • istore_1: 代表引用地址
      • iload_1和iload_2:代表将1和2加载到内存
      • iadd:代表将1和2相加
      • istore_3:代表结果的引用地址并返回出去

JVM的生命周期

虚拟机的启动

  • java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的
  • 说明:就是如果要加载一个自己的类,需要依赖于其他的基础类,虚拟机启动加载其他的基础类称为初始类

虚拟机的执行

  • 程序开始执行虚拟机才运行,程序结束时虚拟机停止
  • 当在执行一个所谓的Java程序时候,真真正正执行的是一个叫做Java虚拟机的进程

虚拟机的退出

  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致java虚拟机进程终止
  • 线程调用System的exit()方法或者Runtime类的halt()方法
  • JNI规范描述了用JNI Invocation API来加载或者卸载 Java虚拟机时候,Java虚拟机的退出情况

Java虚拟机的发展历程

Sun Classic VM

  • 在1996年Java1.0版本的时候,Sun公司发布了一款名为Sun Classic VM的Java虚拟机,同时也是世界上第一款商用的Java虚拟机,JDK1.4之后完全被淘汰
  • 这款虚拟机只提供了解释器,如果使用JIT编译器,就需要进行外挂,但是一旦使用JIT编译器,JIT就会接管虚拟机的执行系统,解释器就不再工作,解释器和编译器不能配合工作
  • 现在的hotspot内置了此虚拟机

Exact VM

  • 为了解决上一个虚拟机的问题,jdk1.2时,sun提供了此虚拟机
  • Exact Memory Management:准确是内存管理
    • 也可以叫Non-Conservative/Accurate Memory Management
    • 虚拟机可以知道内存中某个位置的数据具体是什么类型
  • 具备现代高性能虚拟机的雏形
    • 热点探测
    • 编译器和解释器混合工作模式
  • 只在Solaris平台短暂使用,其他平台上还是classic vm
    • 终被Hotspot取代

Hotspot VM(流行的三大虚拟机之一)

  • Hotspot的历史
    • 最初是由一家名为“Longview Technologies”的小公司设计
    • 1997年,此公司被Sun收购,2009年,Sun公司被甲骨文收购
    • JDK1.3时,Hotspot VM成为默认虚拟机
  • 目前Hotspot占有绝对的市场地位
    • 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是Hotspot
    • Sun/Oracle,OpenJDK的默认虚拟机
    • Hotspot独有的方法区
  • 从服务端,桌面到移动端,嵌入式都有应用
  • Hotspot指的是热点代码探测技术
    • 通过计数器找到最具编译价值的代码,触发即时编译或栈上替换
    • 通过编译器与解释器协同工作,在最优化的程序响应时间和最佳执行性能中取得平衡

JRockit(流行的三大虚拟机之一)

  • 专注于服务器端应用
    • 它可以不太关注程序启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行
  • 大量的行业基准测试显示,JRockit是世界上最快的虚拟机
  • 优势:全面的Java运行时解决方案组合
    • JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或者微妙级的JVM响应时间,适合财务,军事指挥,电信网络的需要
    • MissionControl服务套件,他是一组以极低的开销来监控,管理和分析生产环境中的应用程序的工具
  • 2008年,BEA被Oracle收购
  • Oracle表达了整合两大优秀虚拟机的工作,已经在JDK8中完成。在Hotspot的基础上,移植JRockit的优秀特性
  • 高斯林(Java之父):目前就职于谷歌,研究人工智能和水下机器人

J9(流行的三大虚拟机之一)

  • 全称:IBM Technology for Java Virtual Machine,简称IT4J,内部代号:J9
  • 市场定位于Hotspot接近
  • 广泛用于IBM的各种Java产品
  • 目前,有影响力的三大商用虚拟机之一,也号称世界上最快的Java虚拟机
  • 2017年左右,IBM发布了开源J9 VM,命名为OpenJ9,交给Eclipse基金会管理,也称为Eclipse OpenJ9

解释器和JIT即时编译器的区别

  • 他们都是将字节码文件转换成本地机器指令
  • 但是解释器的执行起步速度快,相当于去旅游,步行说走就走,起步快。而JIT编译器相当于坐公交车去,因为要等公交车,起步慢,但是一旦起步速度也很快,所以如果使用只使用JIT即时编译器,在应用启动的时候需要花很多的时间,而解释器就会快一点,但如果只使用解释器,遇到一些重复的代码,不会缓存,会重复解释,所以一般java虚拟机的执行引擎一般使用解释器和即时编译器来一起使用

其他Java虚拟机的介绍

KVM

  • Oracle在Java ME产品线上的两款虚拟机为:CDC、CLDC HotSpot Implementation VM
  • KVM(Kilobyte)是CLDC-HI早期的产品
  • KVM简单,轻量,高度可移植性,面向更低端的移动设备上还维持自己的一片市场
    • 智能控制器,传感器
    • 老人手机,经济欠发达的地区的手机

Azul VM

  • Azul VM和BEA Liquid VM是与特定硬件平台绑定,软硬件配合的专有虚拟机,是高性能Java虚拟机中的战斗机
  • Azul VM是Azul Systems公司在HotSpot基础上进行大量改进,运行于自己公司专有硬件Vega系统上的Java虚拟机
  • 每个Azul VM实例都可以管理至少数十个CPU和数百G内存的硬件资源,并提供在巨大范围内实现可控的GC时间的垃圾收集器,专有硬件优化的线程调度等优秀特征

Liquid VM

  • 高性能的Java虚拟机中的战斗机
  • BEA公司开发的,直接运行在自家Hypervisor系统上
  • Liquid VM不需要操作系统的支持,自己可以实现线程调度,文件系统,网络支持等

Apache Harmony

  • Apache也曾经推出过与JDK1.5和JDK1.6兼容的Java运行平台Apache Harmongy
  • 他是IBM和Intel联合开发的开源JVM,受到同样开源的OpenJDK的压制,Sun坚决不让Harmongy获得JCP认证,最终与2011年退役,IBM转而参与OpenJDK
  • 虽然目前Apache Harmongy没有被大规模商用,但是它的Java类库代码吸纳进了Android SDK

Microsoft JVM

  • 微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM
  • 只能在window平台下运行,但确是当时Windows下性能最好的Java VM
  • 1997年,Sun以侵权商标,不正当竞争罪名指控微软成功,陪了Sun公司很多钱,微软在WindowsXP SP3中抹掉了器VM所以现在windows上安装了JDK都是HotSpot

TaobaoJVM

  • 由AliJVM团队发布,阿里,国内使用Java最强大的公司,覆盖云计算,金融,物流,电商等众多领域,需要解决高并发,高可用,分布式的符合问题,是整个阿里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版本全部替换掉了

Dalvik VM

  • 谷歌开发的,应用于Android系统,并在Android2.2中提供了JIT,发展迅猛
  • Dalvik VM只能称作虚拟机,而不能称作Java虚拟机,它没有遵循Java虚拟机规范
  • 不能直接执行Java的Class文件
  • 基于寄存器架构,不是jvm的栈架构
  • 执行的是编译以后的dex文件,执行效率比较高
    • dex文件可以通过class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分Java API等
  • Android5.0使用支持提前编译(Ahead Of Time Compilation,AOT)的ART VM替换Dalvik VM

Graal VM

  • 2018年4月,Oracle Labs公开了Graal VM ,号称“Run Programs Faster Anywhere” ,勃勃野心
  • Graal VM在HotSpot VM基础上增强而成的跨语言全栈虚拟机,可以作为任何语言的运行平台
  • 支持不同语言中混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件
  • 工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中间表示,Graal VM提供Truffle工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化,获得比原生编译器更优秀的执行效率

JVM内存结构

类的加载

类的加载过程

  1. 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
  2. ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
  3. 加载的类信息存放在一块被称为方法区的内存空间中。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包含字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

JVM中DNA元数据模板的加载过程

  1. 编译好的class文件存放在本地硬盘上,相当于一个模板,类加载器根据这个class文件向JVM中生成DNA元数据模板,放在方法区。而Class Loader充当一个运输二进制流的快递员。
  • 加载.class文件的方式
    • 从本地系统中加载
    • 通过网络获取,如Web Applet
    • 从zip压缩包读取,成为jar,war格式的基础
    • 运行时计算生成,如动态代理技术
    • 由其他文件生成,如JSP应用
    • 从专有数据库中提取.class文件
    • 从加密文件中获取,典型的防Class文件被反编译的保护措施

Class Loader加载的三个步骤

  • 加载(生成大的Class对象)
  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 链接
  1. 验证(Verify):
    • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载的类的正确性,不会危害虚拟机自身安全
    • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
  2. 准备:
    • 为类变量分配内存并且设置该类变量的默认初始值,即0值
    • 这里不包括使用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
    • 这里不会为实例变量初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到Java堆中
  3. 解析
    • 将常量池内的符号引用转换为直接引用的过程
    • 事实上,解析操作往往会伴随着JVM在执行完初始化之后在执行
    • 符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在<Java虚拟机规范>的Class文件格式中,直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
  • 初始化

    • 初始化阶段就是执行类构造器方法的过程

    • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,如果类中没有定义静态代码块或者静态变量,就不会生成clinit方法(可以在IDEA中使用插件jclasslib查看)

    • 构造器方法中指令按语句在源文件中出现的顺序执行

      • 示例

      • public class Test44 {
            private static int i = 1;
            static {
                i = 2;
                q = 1;
            }
            private static int q = 2;
            public static void main(String[] args) {
            }
        }
        
    • ()不同于类的构造器。(构造器是虚拟机视角下的())

    • 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕

      • public class Test44 {
            static class Father{
                public static int i = 1;
            }
            static class Son extends Father{
                private static int q = i;
            }
        }
        
    • 虚拟机必须保证一个类的()方法在多线程下被同步加锁

      • 类的方法是线程安全的,只会被其中一个线程执行

      • public class Test44 {
             static class Father{
                 public int q=2;
                public static int i = 1;
                static {
                    i = 3;
                    System.out.println(Thread.currentThread().getName()+"初始化");
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        
            }
            public static void main(String[] args) {
                 new Thread(()->{
                     Father father = new Father();
                     System.out.println(father.q);
                 }).start();
                new Thread(()->{
                    Father father1 = new Father();
                    System.out.println(father1.q);
                }).start();
            }
        }
        

几种类加载器

  • JVM支持两种类型的类加载器,分别是引导类加载器(Bootstrap ClassLoader)和自定义加载器(User-Defined ClassLoader)

    • 引导类加载器是c语言和c++实现的,而其他的加载器都是间接或者直接继承ClassLoader是Java实现的
  • 自定义加载器一般指的是程序中由程序员自定义的一类加载器,但是JVM虚拟机规范没有这么定义,而是将所有派生与抽象类ClassLoader的类加载器都划分为自定义加载器

  • 无论类加载器的类型如何划分,在程序中最常见的类加载器始终只有3个

  • 通常来说,分为引导类加载器,扩展加载器,应用加载器,自定义加载器

    • 这四种加载器是包含关系,不是继承关系
  • //sun.misc.Launcher$AppClassLoader@14dad5dc
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(systemClassLoader);
    
    //sun.misc.Launcher$ExtClassLoader@1b6d3586
    ClassLoader excClassLoader = systemClassLoader.getParent();
    System.out.println(excClassLoader);
    
    //null
    ClassLoader bootClassLoader = excClassLoader.getParent();
    System.out.println(bootClassLoader);
    
    //sun.misc.Launcher$AppClassLoader@14dad5dc,使用应用加载器来加载的
    ClassLoader classLoader = Test44.class.getClassLoader();
    System.out.println(classLoader);
    
    //null--->String使用的是引导类加载器来加载的,说明Java的核心类库都是使用引导类加载器来加载的
    ClassLoader classLoader1 = String.class.getClassLoader();
    System.out.println(classLoader1);
    
启动类加载器
  • 是虚拟机自带的加载器

    • 这个类加载使用c/c++语言实现的,嵌套在JVM内部。是JVM的一部分
    • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar,resources.jar,sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
    • 他不继承自java.lang.ClassLoader,没有父加载器
    • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
    • 出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类
  • 获取具体的加载目录

    • URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
      for (URL urL : urLs) {
          System.out.println(urL);
      }
      /**
      file:/E:/jdk8/jdk1.8.0_60/jre/lib/resources.jar
      file:/E:/jdk8/jdk1.8.0_60/jre/lib/rt.jar
      file:/E:/jdk8/jdk1.8.0_60/jre/lib/sunrsasign.jar
      file:/E:/jdk8/jdk1.8.0_60/jre/lib/jsse.jar
      file:/E:/jdk8/jdk1.8.0_60/jre/lib/jce.jar
      file:/E:/jdk8/jdk1.8.0_60/jre/lib/charsets.jar
      file:/E:/jdk8/jdk1.8.0_60/jre/lib/jfr.jar
      file:/E:/jdk8/jdk1.8.0_60/jre/classes
      */
      
扩展类加载器
  • 也是虚拟机自带的加载器

    • Java 语言编写,有sun.misc.Launcher$ExtClassLoader实现
    • 派生于ClassLoader类
    • 父类加载器是启动类加载器
    • 从java.ext.dirs系统属性所指向的目录中加载类库,或者从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户将自己创建的jar放在此目录下,也会由扩展类加载器加载
  • 获取具体的加载目录

    • String property = System.getProperty("java.ext.dirs");
      System.out.println(property);
      /**
      E:\jdk8\jdk1.8.0_60\jre\lib\ext
      C:\WINDOWS\Sun\Java\lib\ext
      */
      
应用程序加载器(系统类加载器)
  • java 语言编写,有sun.misc.Launcher$AppClassLoader实现

  • 派生于ClassLoader类

  • 父类加载器是扩展类加载器

  • 他负责加载环境变量classpath或系统属性java.class.path指向的路径下的类库

  • 该类库加载是程序中默认的类加载器,一般来说,Java应用的类都是由他来加载的

  • 获取具体的加载目录

    • String property1 = System.getProperty("java.class.path");
      String[] split = property1.split(";");
      for (String s : split) {
          System.out.println(s);
      }
      //D:\java-workspace-idea\JVMDemo\out\production\JVMDemo
      
用户自定义加载器
  • 在java的日常开发中,类的加载几乎是由上面的3种加载器相互配合执行的,在必要的时候,我们需要自定义类加载器,来定制类的加载方式
  • 自定义类加载器的作用
    • 隔离加载类:不同框架之间需要自定义类加载器,不然会出现同包同名而引起的冲突
    • 修改类加载的方式
    • 扩展加载源
    • 防止源码泄漏,字节码文件加密需要自定义类加载器来解密使用
  • 自定义类加载器步骤
    1. 可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器
    2. 在JDK1.2之前,在自定义类加载器时候,总会去继承ClassLoader类并重写loadClass方法,在JDK1.2之后建议把自定义的类加载逻辑写在findClass方法中
    3. 如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样可以避免编写findClass方法以及获取字节码流的方式,使自定义类加载器编写更加简洁。
关于ClassLoader
  • ClassLoader类,他是一个抽象类,其后所有的类加载器都继承自ClassLoader
    • getParent()
      • 返回该类加载器的超类加载器
    • loadClass(String name)
      • 加载名称为name的类,返回结果为java.lang.Class类实例
    • findClass(String name)
      • 查找名称为name的类,返回结果为java.lang.Class类实例
    • findLoadedClass(String name)
      • 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类实例
    • defineClass(String name,byte[] b,int off,int len)
      • 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例
    • resolveClass(Class<?> c)
      • 连接指定的一个Java类
    • 以上的方法都不是抽象方法
  • sun.misc.Launcher他是一个java虚拟机的入口应用
获取类加载器的几种方式
  • 获取当前类的ClassLoader:clazz.getClassLoader() clazz指的是大的Class实例
    • Class.forName("").getClassLoader()
  • 获取当前线程向下文的ClassLoader
    • Thread.currentThread().getContextClassLoader()
  • 获取系统的ClassLoader
    • ClassLoader.getSystemClassLoader()
  • 获取调用者的ClassLoader
    • DriverManager.getCallerClassLoader()

双亲委派机制

  • Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时候才会将他的class文件加载到内存中生成class对象。而且加载某个类的class文件时候,Java虚拟机采用的是双亲委派机制,即把请求交给父类处理,他是一种任务委派模式

双亲委派机制的工作原理

  1. 如果一个类加载器收到了类加载的请求,他并不会去自己先去加载,而是把这个请求交给父类加载器
  2. 如果父类加载器还存在其父类加载器,则进一步委托,依次递归,最终到达启动类加载器为止
  3. 如果父类加载器可以完成类的加载,就成功返回,如果不能,子加载器才会去加载。
  4. 如果一个接口是rt.jar包下的,而实现类如jdbc是在第三方jar下的,那么接口是由启动类加载器加载,第三方实现类是由线程的上下文的加载器(一般是应用加载器)加载的

优势

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改

沙箱安全机制

  • 自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类会优先加载jdk自带的String类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制

类的加载补充(类的主动使用与被动使用)

  • 在JVM中表示两个class对象是否为同一个类存在两个必要条件

    • 类的完整类名必须一致,包括包名
    • 加载这个类的ClassLoader(指ClassLoader实例对象)必须一样
  • 对类加载器的引用

    • JVM必须知道一个类是由启动加载器加载的还是由用户类加载器加载的。如果一个类是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
  • 类的主动使用与被动使用

    • 主动使用,分为七种情况
      1. 创建类的实例
      2. 访问某个类或者接口的静态变量,或者对该静态变量赋值
      3. 调用类的静态方法
      4. 反射(Class.forName(""))
      5. 初始化一个类的子类
      6. Java虚拟机启动时候被标明为启动类的类
      7. JDK7开始提供的动态语言支持:
        • java.lang.invoke.MethodHandle实例的解析结果
        • REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化
    • 除了以上的七种情况,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化

运行时数据区

  • 内存是非常重要的系统资源,是硬盘和CPU的中间厂库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请,分配,管理的策略,保证了JVM的搞笑稳定运行。不同的JVM对于内存的划分和管理机制存在部分差异

  • 每一个JVM只有一个Runtime实例。即为运行时环境。相当于运行时环境。

  • Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些随着虚拟机启动而创建,随着虚拟机退出而销毁,另外一些则是与线程一一对应的,这些与线程对应的数据区会随着线程开始和结束而创建和销毁

  • 每个线程单独拥有java栈,本地方法栈,程序计数器,共享堆,方法区(堆外内存,永久代或元空间,代码缓存)

JVM中线程

  • JVM中线程分为守护线程和普通线程

  • 线程是一个程序里的运行单元,JVM允许一个应用有多个线程并行的执行。

  • 在Hotspot JVM里面,每个线程都与操作系统的本地线程直接映射。

    • 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程的run方法

JVM系统线程

  • 如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[] args)的main线程以及所有这个main线程自己创建的线程。
  • 这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
    • 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现,这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程执行类型包括‘stop-the-world’的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
    • 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
    • GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
    • 编译线程:这种线程在运行时会将字节码编译成本地代码
    • 信号调度线程:这种线程接收信号并发生给JVM,在他的内部通过调用适当的方法进行处理。

PC寄存器(程序计数器)

  • JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器中才能运行。

  • 这里并非广义上所指的物理寄存器,也称程序钩子。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟

  • 作用:

    • PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。有执行引擎读取下一条指令。

介绍

  • 他是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
  • 在JVM规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期和线程的生命周期保持一致
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值。
  • 他是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释器工作时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 他是唯一一个在Java虚拟机规范中没有规定任何OutOtMemoryError(也就是内存溢出)情况的区域。其他的java栈,本地方法栈,方法区,堆都存在内存溢出的情况

pc寄存器的使用举例

pc寄存器常见的两个面试题

使用PC寄存器存储字节码指令地址有什么用?

  • 因为CPU需要不停的切换各个线程,这个时候切换回来以后,就得知道接着从哪开始继续执行。
  • JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

PC寄存器为什么会被设定为线程私有的

  • 所谓的多线程是指在一个特定的时间段内只会执行其中莫一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或者恢复,为了能够准确记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样就不会出现相互干扰的情况

CPU时间片

  • CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作他的时间片
  • 并行:相对于线程来说的,多个线程同时执行
  • 串行:相对于线程来说,多个线程排队执行,一次只执行一个线程
  • 并发:相对于CPU执行来说的,CPU快速切换来执行不同的线程

虚拟机栈

  • 栈是运行时的单位,而堆是存储的单位
  • 栈解决程序的运行问题,即程序如何执行,或者如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪儿。
  • Java虚拟机栈是什么?
    • Java虚拟机栈,早期也叫Java栈。每个线程在创建的时候都会去创建一个虚拟机栈,其内部保存一个个的栈贞(Stack Frame),对应着一次次的Java方法的调用
    • 是线程私有的
  • 生命周期
    • 生命周期和线程一样
  • 作用
    • 主管Java程序的运行,它保证方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回
  • 栈的优点
    • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
    • 对于栈来说不存在GC,只存在OOM(内存溢出)

java虚拟机栈中常见的异常和如何设置栈大小

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常
  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请足够的内存,或者在创建新的线程时候没有足够的内存去创建对应的虚拟机栈,就会报OutOfMemoryError异常

设置栈内存的大小

  • 我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

栈的存储结构和运行原理

存储结构

  • 每个线程都有自己的栈,栈中的数据都是以栈贞(Stack Frame)的格式存在的
  • 在这个线程上正在执行的每个方法都各自对应一个栈贞。
  • 栈贞是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈的运行原理

  • 在一条活动线程中,一个时间点上,只会有一个活动的栈贞,即只有当前正在执行的方法的栈贞是有效的,这个栈贞被称为当前栈贞(Current Frame),与当前栈贞对应的是当前方法,定义这个方法的类就是当前类

  • 执行引擎运行的所有字节码指令只针对当前栈贞进行操作

  • 如果在该方法中调用了其他方法,对应的新的栈贞会被创建出来,放在栈的顶端,成为新的当前贞。

  • 不同线程中所包含的栈贞是不允许存在相互引用的,即不可能在一个栈贞之中引用另外一个线程的栈贞

  • 如果当前方法调用了其他方法,方法返回之际,当前栈贞会传回此方法的执行结果给前一个栈贞,接着,虚拟机会丢弃当前栈贞,使得前一个栈贞重新成为当前栈贞。

  • Java方法有两个返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常,不管使用那种方式,都会导致栈贞被弹出。

栈贞的内部结构

  • 每个栈贞都存储着
    • 局部变量表
    • 操作数栈(表达式栈)
    • 动态链接(运行时常量池的方法引用)
    • 方法返回地址
    • 一些其他的附加信息
    • 其中动态链接,方法返回地址,一些附加信息被统一称为栈贞的数据区
局部变量表
  • 局部变量表也被称之为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体中的局部变量,这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型
  • 由于局部变量表是建立在线程的栈上的,是线程私有的的数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期间确定下来的,并保存在方法Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
  • 局部变量表中的变量只在当前方法调用中有效,在方法执行时候,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈贞的销毁,局部表量表也会随之销毁
局部变量表的具体的内部结构
  • 测试代码(使用jclasslib插件来查看main方法的局部变量表)
public static void main(String[] args) {
    int q = 10;
    print();
    System.out.println(q);
}
public static void print(){
    int i = 1;
    System.out.println(i);
}
  • Code中有LineNumberTable是代码中的行号对应的具体的字节码指令的行号

  • 局部变量表中的具体变量

  • L代表引用类型,i代表int类型
局部变量表中的变量槽slot的理解
  • 参数值的存放总是在局部变量数组的index0开始的,到数组长度-1的索引结束

  • 局部变量表,最基本的存储单元为slot(变量槽)

  • 局部变量表存放编译期间可知的各种基本数据类型,引用类型,returnAddress类型的变量

  • 在局部变量表中,32位以内的类型只占用一个slot:包括returnAddress类型,64位的类型占用两个slot:包括long,double

    • byte,short,char在存储前被转换为int,boolean也被转换为int
    • long,double则占据两个slot
  • JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

  • 当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上

  • 如果需要访问局部变量表中的一个64位的局部变量值时候,只需要使用前一个索引即可

  • 如果当前栈贞是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

  • 栈贞中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期的槽位,从而达到节省资源的目的。

    • public void test(){
          int a=1;
          {
              int b=1;
              b=b+a;
          }
          int c = a+1;
      }
      //从下面的字节码分析b过期之后,新定义的c会重用b的槽位
      
局部变量和成员变量的区别
  • 成员变量(类变量和实例变量)在类加载和初始化的时候有默认的初始化值
  • 局部变量JVM不会自动为其初始化值,所以定义局部变量的时候需要手动赋值,赋值编译不通过。
局部变量表的补充说明
  • 在栈贞中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的对象都不会被回收
操作数栈
  • 每一个独立的栈贞中除了包含局部变量表以外,还包含操作数栈,也称之为表达式栈

  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据(由执行引擎来执行)

    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈
    • 比如:执行复制,交换,求和等操作
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时存储空间

  • 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈贞也会随之被创建出来,这个方法的操作数栈是空的

  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需要的最大深度在编译期间就已经定义好了,保存在方法的Code属性中,为max_stack的值

  • 栈中保存的数据可以是java的任意数据类型

    • 32位的类型占用一个栈单位深度
    • 64位的类型占用两个栈单位深度
  • 操作数栈采用标准的入栈和出栈来操作数据的,并非采用访问索引的方式来进行数据访问。

  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈贞的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令

  • 操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证

  • 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

操作数栈的字节码指令执行分析
public void test(){
    int i=10;
    int b=100;
    int sum = i+b;
}
  • 对于以上方法的字节码分析
    • 当方法初始化的时候,局部变量表和操作数栈都是空的,对于定义的变量的值会先进入操作数栈(方便以后操作),然后再存储在局部变量表中
 0 bipush 10 //将10入栈
 2 istore_1 //将10存储到局部变量表的1号索引(0号索引是当前对象的实例引用)
 3 bipush 100 //将100入栈
 5 istore_2 //将100存储到局部变量表的2号索引
 6 iload_1 //从操作数栈中弹出数据
 7 iload_2
 8 iadd //将弹出的数据进行相加操作
 9 istore_3 //将结果存储到局部变量表的3号索引
10 return //返回
栈顶缓存技术
  • 前面提过,基于栈式架构的虚拟机所使用的的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时意味着将需要更多的指令分派次数和内存对鞋次数
  • 由于操作数是存储在内存中的,因此频繁的执行内存读写操作必然会影响执行速度,为了解决这个问题,Hotspot JVM的设计者提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,依次对内存的读写次数,提升执行引擎的执行效率
动态链接(指向运行时常量池的方法引用)
  • 每一个栈贞内部都包含一个指向运行时常量池中该栈贞所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接,比如:invokedynamic指令
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池中。比如:描述一个方法调用了另外的其他方法时候,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

  • 常量池
    • 常量池的作用就是提供一些符号和常量,便于指令的识别

举例

public class DemoDemo {
    public static void main(String[] args) {
        test();
    }
    public static void test(){
        int i=10;
        int b=100;
        int sum = i+b;
    }
}
  • 对应的main方法的字节码指令

  • 对应的invokestatic字节码指令的#2就是指向运行时常量池的对应的方法test的引用
  • 在常量池中#2对应的就是#3.#24
  • #3对应#25,#25是当前类
  • #24对应#16:#6
  • #16对应test方法,#6对应返回类型为()V ,也就是void
  • 通过以上的符号引用找到了真实的方法直接引用(动态链接的作用)
方法的绑定机制:静态绑定与动态绑定
  • 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关

静态链接和动态链接(早期绑定与晚期绑定)

  • 当一个字节码文件被装载进JVM内部时候,如果被调用的目标方法在编译期间可知,且运行期间保持不变时候,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
  • 如果被调用的方法在编译器期间无法被确定下俩,也就是说,只能够在程序运行期间将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,称之为动态链接
  • 对应的方法的绑定机制为:早期绑定和晚期绑定,绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
  • 随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差异,但是他们彼此之间始终保持着一个共性,就是支持封装,继承,多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式
  • Java中任何一个普通的方法其实够具备虚函数的特性,他们相当于c++语言中的虚函数(c++中则需要使用关键字virtual来显式定义),如果在Java程序中不希望某个方法拥有虚函数的特征,则可以使用关键字final来标记
四种方法调用指令区分非虚方法和虚方法

非虚方法

  • 如果方法在编译期间就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法
  • 静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法,其他的称为虚方法

虚拟机中提供的以下几条方法调用指令

  • 普通调用指令
    1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
    2. invokespecial:调用方法,私有及父类方法,解析阶段确定唯一方法版本
    3. invokevirtual:调用所有虚方法(除去使用final的方法)
    4. invokeinterface:调用接口方法
  • 动态调用指令
    5. invokedynamic:动态解析出需要调用的方法,然后执行
  • 前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的称为虚方法(除去final修饰的方法也是invokespecial)
关于invokedynamic指令
  • JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这就是Java为了实现动态类型语言支持而做的一种改进

    • Java整体属于静态语言,Landam表达式的引入具有一定的动态语言的特性

    • 动态类型语言和静态类型语言两者的区别就在于类型的检查是在编译器还是在运行期,满足前者就是静态类型语言,反之是动态类型语言

    • 静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息

    • 如Java和JavaScript的定义变量的区别

      //在编译期间就检查变量类型
      Java String info = "123"
      //在运行期间才检查变量值的类型
      JavaScript var info ="12" info=123
      
  • 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令,直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式(使用Lambda表达式可以生成invokedynamic指令)

  • Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器

方法重写的本质与虚方法表的使用
  • java语言中方法重写的本质
    1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
    2. 如果在C中找到了与常量中描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,如果没有权限,则报错java.lang.IllegalAccessError异常
    3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的方法查找
    4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
  • IllegalAccessError介绍:
    • 程序试图访问或者修改一个属性或调用一个方法,这个属性或方法,你没有权限访问,一般会引起编译器异常。如果发生在运行时,说明一个类发生了不兼容的改变
    • 常常出现在Maven的jar包冲突中

虚方法表

  • 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现。使用索引来代替查找
  • 每一个类都有一个虚方法表,表中存放着各个方法的实际入口
  • 虚方法表什么时候创建
    • 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成后,JVM会把类的方法表也完成初始化

  • 一个类中所有虚方法都有了指向,需要用的时候直接引用即可,不用依次往上查找对比找出对应的方法,提高了执行效率
方法返回地址
  • 存放调用该方法的pc寄存器的值
  • 一个方法的结束
    • 正常执行完成
    • 出现未处理的异常,非正常退出
  • 无论通过那种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时候,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈贞一般不会保存这部分信息

正常退出和异常退出的区别

  • 正常完成退出和异常完成退出的区别在于通过异常完成退出的不会给他的上层调用者产生任何的返回值。

方法的正常退出

  • 一个方法的正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定
  • 在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short,int)lreturn(long),freturn(float),dreturn(double),areturn(引用类型)。另外还有一个return指令返回指令为void的方法,实例初始化方法,类和接口的初始化方法使用。

方法的异常

  • 在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是说只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口
  • 方法在执行过程中抛出异常时的异常处理,存储在一个异常处理表中,方便在发生异常的时候找到处理异常的代码

  • 如果异常没有被处理直接跳到return语句
栈贞中的一些附加信息
  • 栈贞中还允许携带与Java虚拟机实现相关的一些附加信息,例如,对程序调试提供支持的信息
虚拟机栈常见的面试题
  1. 举例栈溢出的情况(StackOverflowError)

    • 通过-Xss设置栈的大小
  2. 调整栈大小,就能保证不出现溢出嘛 不能

  3. 分配的栈内存越大越好嘛 不是

  4. 垃圾回收是否会涉及到虚拟机栈 不会

  5. 方法中定义的局部变量是否线程安全 具体问题具体分析

    //StringBuilder线程安全
    public void method2(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("1");
        stringBuilder.append("2");
    }
    //StringBuilder线程安全,但是返回出去的String类型就不是线程安全
    public String method3(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("1");
        stringBuilder.append("2");
        return stringBuilder.toString();
    }
    //StringBuilder是从外部传递过来的,是非线程安全的
    public void method4(StringBuilder stringBuilder){
        stringBuilder.append(1);
        stringBuilder.append(2);
    }
    //返回出去的stringBuildr也是非线程安全的
    public StringBuilder method5(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("1");
        stringBuilder.append("2");
        return stringBuilder;
    }
    

本地方法栈

基本介绍

  • Java虚拟机用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
  • 是线程私有的
  • 和虚拟机栈一样可以实现固定或者是可动态扩展的内存大小
  • 他的具体做法是在本地方法栈中登记本地方法,在执行引擎中执行加载本地方法库
  • 当某个线程调用一个本地方法的时候,他就进入了一个全新的并且不再受虚拟机限制的世界。他和虚拟机拥有同样的权限。
    1. 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    2. 它甚至可以直接使用本地处理器中的寄存器
    3. 直接从本地内存的堆中分配任意数量的内存
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等。
  • 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一

本地方法

基本介绍

  • 简单来讲,一个native方法就是一个java调用非java代码的接口。一个本地方法是这样一个Java方法:该方法的实现由非java语言实现,比如c,这个特性并非java所特有,很多其他的编程语言都有这一机制,不如c++中,可以使用extern "C"告知C++编译器去调用一个c的函数
  • 在定义一个本地方法的时候,并不提供实现体,因为其实现体是由非java语言在外面实现的。
  • 本地方法的作用是融合不同的编程语言为java所用,他的初衷是融合c/c++
  • 标识符native可以和所有的其他的java标识符连用,但是abstract不能一起使用

为什么使用本地方法

  • java使用过起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意的时候,问题就来了
  • 与java环境外交互
    • 有时候Java应用需要与java外面的环境交互,这是本地方法存在的主要原因。如java需要与一些底层系统或者硬件交换信息的情况,本地方法正是这样一种交流机制:他为我们提供了一个非常简介的接口,二而且我们无需去了解Java应用之外的繁琐的细节
  • 与操作系统的交互
    • JVM支持着Java语言本身和运行时库,他是Java程序赖以生存的平台,他由一个解释器和一些连接到本地代码的库组成。然而不管怎样,他毕竟不是一个完整的系统,他经常依赖于一底层系统的支持。这些底层系统内常常是强大的操作系统。通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。还有,如果我们要使用一些Java语言本身没有提供封装的操作系统特性时候,我们也需要使用本地方法。
  • Sun's Java
    • Sun的解释器是用C实现的,这使得它能像一些普通的c一样与外部交互。jre大部分是用java实现的,他也通过一些本地方法与外界交互。例如:类java.lang.Thread的setPriority()方法是用Java实现的,但是他的实现调用的是该类的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在windows 95的平台上,这个本地方法最终将调用wind32 setPriority() API.这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库提供,然后被JVM调用

本地方法的现状

  • 目前该方法使用的越来越少了,除非是与硬件相关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等

堆

堆空间的概述

基本介绍

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域

  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大的一块内存空间

    • 堆内存的大小是可以被调节的
    • 使用-Xms 10m -Xmx10m来设置大小
  • Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但是在逻辑上他应该被视为连续的

  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)

  • java虚拟机规范中对Java堆的描述是:所有的对象实例以及数组都应该在运行时分配在堆上。

    • 但是从实际的角度看:几乎所有的对象实例都在堆中分配内存,后面还可以在栈上分配
  • 数组和对象可能永远不会存储在栈上,因为栈贞中保存引用,这个引用指向对象或者数组在堆中的位置

  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除

  • 堆,是GC执行垃圾回收的重点区域

堆的细分内存结构

基本介绍

  • Java7之前堆内存逻辑上分为三部分:新生区,养老区,永久区

  • Java8之后堆内存逻辑上分为:新生区,养老区,元空间

    • 新生区又分为Eden区和Survivor区
    • 元空间替代了永久区

  • 约定:

    • 新生区<->新生代<->年轻代
    • 养老区<->老年区<->老年代
    • 永久区<->永久代
  • 堆内存

  • 堆空间从理论上是分为三部分,但是实际上堆空间只管理新生区和养老区

堆空间大小的设置和查看

基本介绍

  • Java堆区用于存储Java对象实例,堆的大小在JVM启动的时候就已经设定好了
    • -Xms:用于表示堆区的起始内存,等价于-XX:InitialHeapSize
    • -Xmx:用于表示堆区的最大内存,等价于-XX:MaxHeapSize
    • 通常情况下手动设置堆空间的时候,起始内存和最大内存设置为一样的
      • 其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
  • 一旦堆区中的内存大小超过所设定的最大内存,将会抛出OutOfMemoryError异常
  • 默认情况下,初始内存大小为电脑内存大小的64分之一,最大内存为电脑的4分之一

使用设置堆区的大小

  • 使用-Xms和-Xmx设置新生代和老年代的内存大小

    • -X是jvm的运行参数
    • ms是memory start

  • 查看设置的堆区的内存大小

    • 方式一:使用jps(jdk中的命令,用于查看java运行进程) 和 jstat -gc 进程id查看

    • 方式二:使用-X:+PrintGCDetails命令查看程序运行完成后的内存情况

新生代与老年代中相关的参数的设置

基本介绍

  • 存储在JVM中的Java对象可以被划分为两类

    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡多非常迅速
    • 另一类对象的生命周期非常长,在一些极端情况下能和JVM的生命周期保持一致,如数据库连接对象
  • Java堆区进一步细分,可以划分为年轻代和老年代

    • 其中年轻代又可以划分为Eden空间,Survivor0空间(from区),Survivor1空间(to区)

  • 配置新生代和老年代在堆结构中的占比

    • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
    • 可以通过-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
    • 可以通过命令jps和jinfo -flag NewRatio 进程号来查看新生区和老年区的比例,默认为1:2
  • 配置新生代中的eden区和survivorratio的比例

    • 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
      • 但是实际上是6:1:1
    • 可以通过-XX:SurvivorRatio=8来强行指定比例为8:1:1
    • 几乎所有的Java对象都是在Eden区被创建出来的
    • 绝大部分的Java对象的销毁都在新生代进行的
      • IBM公司的研究表明:在新生代中80%的对象都是朝生夕死的
    • 可以使用-Xmn设置新生代的最大内存大小
      • 一般使用默认值就可以了(如果使用-XX:NewRatio=2设置了比例,优先听从-Xmn的)

图解对象分配的一般过程

基本介绍

  • 为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存后是否在内存空间中产生内存碎片

为对象分配内存步骤

  1. new的对象先放在伊利园区
  2. 当伊利园区空间填满时候,程序又需要创建对象,JVM的垃圾回收器将对伊利园进行垃圾回收,将伊利园区中的不再被引用的对象销毁,再加载新的对象放到伊利园区
  3. 然后将伊利园区中剩余对象移动到幸存者0区
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区,如果没有回收,就会放到幸存者1区
  5. 如果再次经历垃圾回收,此时会重新回幸存者0区,接着再去幸存者1区
  6. 什么时候可以去养老区,可以设置次数,默认为15次
    • -XX:MaxTenuringThreshold=进行设置
  7. 在养老区,相对悠闲,当养老区内存不足时候,再次触发GC:进行养老区的内存清理
  8. 若养老区执行了GC之后发现依然无法进行对象的保存,就会产生OOM异常

![](C:\Users\CLT\Pictures\Screenshots\屏幕截图 2023-04-20 124432.png)

总结

  • 针对幸存者s0,s1区的总结:复制之后有交换(复制算法),谁空谁是to
  • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区(元空间)收集
posted @ 2023-08-03 22:19  在赶路的我  阅读(27)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3