JVM整体知识体系深度学习

文章目录

  • JVM整体知识体系深度学习
    • 一、JVM与Java体系结构。
      • 1.1、JVM简介
      • 1.2、JVM的位置
      • 1.3、JVM整体结构
      • 1.4、JVM架构模型
      • 1.5、JVM生命周期
      • 1.6、JVM发展历程
        • 1、Sun Classic VM
        • 2、Exact VM
        • 3、HotSpot VM
        • 4、JRockit
        • 5、J9
        • 6、KVM和CDC/CLDC Hotspot
        • 7、Azul VM
        • 8、Liquid VM
        • 9、Apache Harmony
        • 10、Microsoft JVM
        • 11、Taobao JVM
        • 12、Dalvik VM
    • 二、类加载子系统
      • 2.1、内存结构
      • 2.2、类加载器与类加载过程
        • 1、类加载器:
        • 2、类加载过程:
      • 2.3、类加载器的分类
        • 1、引导类加载器【又称启动类加载器 Bootstrap ClassLoader】
        • 2、拓展类加载器【Extension ClassLoader】
        • 3、系统类加载器【AppClassLoader|SystemClassLoader】
        • 4、用户自定义类加载器
        • 2.4、关于ClassLoader
        • 2.5、双亲委派机制
        • 2.6、双亲委派机制优点
        • 2.7、沙箱安全机制
    • 三、运行时数据区概述及线程
      • 3.1、内存的概念
      • 3.2、数据区内部线程所属
      • 3.3、线程
    • 四、程序计时器 (PC寄存器)
      • 4.1、简介
      • 4.2、区域分析
      • 4.3、常见问题
      • 4.4、区分并行、并发、串行
    • 五、虚拟机栈
      • 5.1、虚拟机栈出现的背景
      • 5.2、内存中的栈与堆
      • 5.3、简介
      • 5.4、栈可能出现异常情况
      • 5.5、设置栈内存大小
      • 5.6、栈的储存单位
      • 5.7、栈运行原理
      • 5.8、栈帧的内部结构
        • 1、局部变量表【Local Variables】
        • 2、关于Slot的理解
        • 3、操作数栈【Operand stack】
        • 4、代码追踪
        • 5、动态链接 【或指向运行时常量池的方法引用】
        • 6、方法返回地址
        • 7、附加信息【不确定存在】
      • 5.9、栈顶缓存技术【Top-Of-Stack-Cashing】技术。
      • 5.10、方法的调用
      • 5.11、虚拟机栈的五道面试题
        • 1、举例栈溢出的情况 【StackOverflowError】
        • 2、调整栈大小、就能保证不出现溢出么?
        • 3、分配的栈内存越大越好么?
        • 4、垃圾回收是否涉及到虚拟机栈?
        • 5、方法中定义的局部变量是否线程安全?
    • 六、本地方法接口
      • 6.1、所处位置
      • 6.2、什么是本地方法?
      • 6.3、为什么使用本地方法
      • 6.4、现状
    • 七、本地方法栈
    • 八、堆 (Heap)【重点】
      • 8.1、堆的核心概述
      • 8.2、栈堆方法区关系
      • 8.3、内存细分
      • 8.4、设置堆内存大小与OOM
      • 8.5、新生代与老年代
      • 8.6、图解对象分配过程
      • 8.7、对象分配的特殊情况
      • 8.8、常见调优工具
      • 8.9、Minor GC、Major GC 、Full GC
      • 8.10、堆空间分代思想
      • 8.11、内存分配策略【对象提升`Promotion`规则】
      • 8.12、小结堆空间的参数设置
      • 拓展:堆是分配对象存储的唯一选择么?
    • 九、方法区【重点】
      • 9.1、栈、堆、方法区的交互关系
        • 整体数据区情况
      • 9.2、方法区的理解
      • 9.3、设置方法区大小与OOM
      • 9.4、如何解决这些OOM 【在后面具体讲。此处简述】
      • 9.5、方法区的内部结构
        • 1、类型信息
        • 2、域信息/属性信息
        • 3、方法的信息
        • 4、non-final的类变量
        • 5、运行时常量池
      • 9.6、方法区的演进细节
      • 9.7、永久代为什么要被元空间替换
      • 9.8、StringTable 【字符串常量池\字符串字面量】为什么要调整?
      • 9.9、方法区的垃圾回收
      • 9.10、运行时数据区的常见大厂面试题。
    • 十、对象的实例化内存布局与访问定位。
      • 10.1、对象的实例化
      • 10.2、对象的内存布局
      • 10.3、对象访问定位
    • 十一、直接内存 【Direct Memory】
    • 十二、执行引擎
      • 12.1、执行引擎概述
      • 12.2、执行引擎的工作过程
      • 12.3、Java代码编译和执行的过程
      • 12.4、理解机器码、指令、汇编语言。
      • 12.5、解释器
      • 12.6、JIT编译器【及时编译器】
    • 十三、StringTable
      • 1、String的基本特性
      • 2、String的内存分配
      • 3、String的基本操作
      • 4、intern()的使用

一、JVM与Java体系结构。

前言

是否遇到过一下问题。

  • 运行的线上系统突然卡死,系统无法访问,甚至直接OOM【OutOfMemory 内存溢出】
  • JVM GC问题
  • 新项目上线前、JVM参数设置。
  • JVM面试原理概念性东西不清楚。调优、GC问题等解决方案不了解。

垃圾收集机制为我们打理了很多繁琐的工作、大大提高了开发的效率 。但是垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高拓展性应用和诊断运行时问题的基础,也是Java工程师进阶的必备能力。

学习参考文章书目:

1、官网文档: The Java Virtual Machine Specification Java SE X Edition

2、深入理解Java虚拟机 (JVM高级特性与最佳实践) 资源地址 包含官方文档

1.1、JVM简介

Java Virtual Machine【Java虚拟机】

所谓虚拟机、就是一台虚拟的计算机。他是一款软件。用来执行一系列虚拟计算机指令。大体上。虚拟机可分为系统虚拟机程序虚拟机。而Java虚拟机是一台执行Java字节码的虚拟计算机。它拥有独立的运行机制,其运行的Java字节码页未必由Java语言编译而成【体现了跨语言性】,JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回收器,以及可靠的及时编译器。

【Java技术的核心就是Java虚拟机】因为所有Java程序都运行在Java虚拟机内部。

  • 比较有名的VisualBox、VMware就属于系统虚拟机,他们完全是对物理计算机的仿真,提供了一个可运行完成操作系统的软件平台。
  • 程序虚拟机的典型代表就是Java虚拟机。它专门为执行单个计算机程序而设计。在Java虚拟机中执行的指令我们称为Java字节码指令。
  • 无论是系统虚拟机还是程序虚拟机、在上面运行的软件都被限制于虚拟机提供的资源中。

如果说Java是跨平台的语言、那么JVM就可以说是跨语言的平台。体现如下图。

主流虚拟机:HotSpot 【Oracle】、JRockit 【Oracle】、J9 【IBM】.

1.2、JVM的位置

JVM是运行在操作系统之上的,它与硬件没有直接的交互。

1.3、JVM整体结构

【简单理解】Class文件通过ClassLoader装载到内存区域进行处理。多线程共享方法区和堆。栈和程序计数器单独运行。程序整体转变为机器指令则是通过执行引擎进行处理。

【复习Java代码执行过程】

1.4、JVM架构模型

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

两种架构之间的区别

  • 基于栈式架构的特点

    • 避开了寄存器的分配难题:使用零地址指令方式分配。
    • 设计和实现更简单,适用于资源受限的系统;
    • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
    • 不需要硬件支持、可移植性更好、更好实现跨平台
  • 基于寄存器的架构特点
    • 典型的应用是x86的二进制指令集:比如传统的Pc以及Android的Davlik虚
    • 指令集架构则完全依赖硬件,可移植性差
    • 性能优秀和执行更高效;
    • 花费更少的指令去完成一项操作。
    • 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令、和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。
//测试代码
package com.max.jvm;public class StackStruTest {public static void main(String[] args) {int i = 1;int j = 2;int count =i + j;System.out.println(count);}
}
//基于栈架构模型
//终端执行反编译。 javap -v StackStruTest.class
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: iconst_1  //常量1入栈1: istore_1    2: iconst_2  //常量2入栈    3: istore_24: iload_15: iload_26: iadd      //常量1、2出栈 执行降价7: istore_3  //结果3入栈8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;11: iload_312: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V15: returnLineNumberTable:line 6: 0line 7: 2line 8: 4line 9: 8line 11: 15LocalVariableTable:Start  Length  Slot  Name   Signature0      16     0  args   [Ljava/lang/String;2      14     1     i   I4      12     2     j   I8       8     3 count   I
}
//基于寄存器架构模型mov eax,2 //讲eax寄存器的值设为1
add eax,3 //将eax寄存器的值加3   

总结:

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,指令多、编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

1.5、JVM生命周期

1、虚拟机的启动

  • Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。

2、虚拟机的执行

  • 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序

  • 程序开始执行时他才运行、程序结束时他就停止。

  • 执行一个所谓的Java程序的时候、真真正正在执行的是一个叫做Java虚拟机的进程。

3、虚拟机的退出。

有以下几种情况

  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止
  • 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法。并且Java安全管理器也允许这次的exit或者halt操作。
  • 除此之外 JNI(Java Native Interface)规范描述了 JNI API来加载或卸载Java虚拟机时、Java虚拟机的推出情况。

1.6、JVM发展历程
1、Sun Classic VM

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

【这代还没有兼容解释器和JIT、所以早期给人的印象是比较慢的。当时考虑因为两者不能共同工作那么必定选其一。 如果单纯使用外挂JIT的话,那么处理字节码文件前置时间会比较长。根源就在于它把所有信息当成热点代码进行处理了。

两者形象的例子就像:去某个地方。走路去和坐公交去。走路虽然慢、但少了等车的不确定时间。坐公交反之亦然。】

2、Exact VM

为了解决上一个虚拟机问题,jdkl.2时,sun提供了此虚拟机。Exact Memory Management:准确式内存管理也可以叫Non-Cconservative/Accurate Memory Management虚拟机可以知道内存中某个位置的数据具体是什么类型。

具备现代高性能虚拟机的雏形

  • 热点探测

  • 编译器与解释器混合工作模式

只在solaris平台短暂使用,其他平台上还是classic vm

英雄气短,终被Hotspot虚拟机替换

3、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指的就是它的热点代码探测技术。

通过计数器找到最具编译价值代码,触发即时编译或栈上替换
通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡

4、JRockit

专注于服务器端应用,它可以不太关注程序启动速度,因此JRockit内部不包含解析器实现,全部代码
都靠即时编译器编译后执行。
大量的行业基准测试显示,JRockit JVM是世界上最快的JVM。

使用JRockit产品,客户已经体验到了显著的性能提高(一些超过了70% )和硬件成本的减少(达50%)。
优势:全面的Java运行时解决方案组合

  • JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒级的JVM响应时间,适合财务、军事指挥、电信网络的需要。
  • MissionControl服务套件【JMC 用于监控内存泄漏】,它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。2008年,BEA被oracle收购。oracle表达了整合两大优秀虚拟机的工作,大致在JDK 8中完成。整合的方式是在HotSpot的基础上,移植JRockit的优秀特性。
5、J9

全称:IBM Technology for Java virtual Machine,简称IT4J,内部代号:J9

  • 市场定位与Hotspot接近,服务器端、桌面应用、嵌入式等多用途VM·广泛用于IBM的各种Java产品。
  • 目前,有影响力的三大商用服务器之一,也号称是世界上最快的Java虚拟机。. 2017年左右,IBM发布了开源J9 VM,命名为openJ9,交给Eclipse基金会管理,也称为 Ecilpse openJ9。
6、KVM和CDC/CLDC Hotspot
  • oracle在Java ME产品线上的两款虚拟机为: CDC/CLDC HotSpot Implementation VM
  • KVM (Kilobyte)是CLDC-HI早期产品
  • 目前移动领域地位尴尬,智能手机被Android和ioS二分天下。
  • KVM简单、轻量、高度可移植,面向更低端的设备上还维持自己的一片市场
    • 智能控制器、传感器
    • 老人手机、经济欠发达地区的功能手机
  • 所有的虚拟机的原则:一次编译,到处运行。
7、Azul VM
  • 前面三大“高性能Java虚拟机”使用在通用硬件平台上
  • 这里Azul VM和BEA Liquid VM是与特定硬件平台绑定、软硬件配合的专有虚拟机。高性能Java虚拟机中的r战斗机
  • Azul VM是Azul systems公司在HotSpot基础上进行大量改进,运行于Azul systems公司的专有硬件vega系统上的Java虚拟机。
  • 每个Azul VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的Gc时间的垃圾收集器、专有硬件优化的线程调度等优秀特性。
  • 2010年,Azul systems公司开始从硬件转向软件,发布了自己的Zing JVM,可以在通用x86平台上提供接近于Vega系统的特性。
8、Liquid VM
  • 高性能Java虚拟机中的战斗机。
  • BEA公司开发的,直接运行在自家Hypervisor系统上
  • Liquid VM即是现在的JRockit VE(Virtual Edition) ,LiquidVM不需要操作系统的支持,或者说它自己本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等。
  • 随着JRockit虚拟机终止开发,Liquid VM项目也停止了。
9、Apache Harmony
  • Apache也曾经推出过与JDK 1.5和JDK 1.6兼容的Java运行平台Apache Harmony。
  • 它是IBM和Intel联合开发的开源JVM,受到同样开源的openJDK的压制, sun坚决不让Harmony获得JCP认证,最终于2011年退役,IBM转而参与openJDK
  • 虽然目前并没有Apache Harmony被大规模商用的案例,但是它的Java类库代码吸纳进了Android SDK。
10、Microsoft JVM
  • 微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM。
  • 只能在window平台下运行。但确是当时windows下性能最好的Java VM。
  • 1997年,sun以侵犯商标、不正当竞争罪名指控微软成功,赔了sun很多钱。微软在windowsXP SP3中抹掉了其VM。现在windows上安装的jdk都是Hotspot。
11、Taobao JVM
  • 由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版本全部替换了。
12、Dalvik VM
  • 谷歌开发的,应用于Android系统,并在Android2.2中提供了JIT,发展迅猛。
  • Dalvik VM只能称作虚拟机,而不能称作“Java虚拟机”,它没有遵循Java虚拟机规范
  • 不能直接执行Java的 class 文件
  • 基于寄存器架构,不是jvm的栈架构。
  • 执行的是编译以后的dex (Dalvik Executable)文件。执行效率比较高。
    • 它执行的dex (Dalvik Executable)文件可以通过class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。
    • Android 5.o使用支持提前编译(Ahead of Time compilation,AOT)的 ART VM替换Dalvik VM。

二、类加载子系统

2.1、内存结构

2.2、类加载器与类加载过程
1、类加载器:

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

① 加载 【Loading】

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

补充:加载 .class文件的方式

  • 从本地系统中直接加载
  • 通过网络获取,典型场景: Web Applet
  • 从Zip压缩包中读取,成为日后jar、war格式的基础
  • 运行时计算生成,使用最多的是:动态代理技术
  • 由其他文件生成,典型场景:JSP应用
  • 从专有数据库中提取.class文件。比较少见
  • 从加密文件中获取,典型的防Class文件被反编译的保护措施。

② 链接【Linking】

1、验证【Verify】

  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。

  • 主要包含四种验证: 文件格式验证,元数据验证,字节码验证,符号引用验证。

2、准备【Prepare】

  • 为类变量分配内存并且设置该类变量的默认初始值,即零值。

  • 这里不包含用final修饰的static、因为final在编译的时候就会分配了,准备阶段会显示初始化。

  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

3、解析【Resolve】

  • 将常量池内的符号引用转换为直接引用的过程。
  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等
    对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等。

③ 初始化【Initialization】

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

  • 此方法不需要定义,是Javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

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

  • () 不同于类的构造器。

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

  • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。

public class ClassInitTest{private static int num = 1;static {num = 2;number = 20;
}private static int number = 10;public static void main(String[] args){System.out.println(ClassInitTest.num)  //2System.out.println(ClassInitTest.number) //10}
}//在程序执行到Prepare阶段时、为类变量分配内存并且设置该类变量的默认初始值,即零值。随后按照加载顺序、进行赋值。

补充: 静态代码块中、对声明的变量进行赋值是可行的。但是在内部进行引用则会报错 【非法的前向引用】

2.3、类加载器的分类
  • JVM支持两种类型的类加载器,分别为引导类加载器【bootstrap ClassLoader】和自定义类加载器【User-Defined ClassLoader】

    这里疑问:Extension Class Loader 【拓展类加载器】 System Class Loader【系统类加载器】 为什么没有单独分类呢?

    可以简单看一下分类的关系图

  • 从概念上来讲、自定义类加载器一般指的是程序中由开发人员自定义的一类 类加载器。但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类都划分为自定义类加载器。所以上述拓展和系统间接包含关系于引导类加载器。

package com.max.jvm;public class ClassLoaderTest {public static void main(String[] args) {//获得系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2//默认系统类加载器ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2//SystemClassLoader上层为拓展类加载器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@4d7e1886//SystemClassLoader上层为拓展类加载器ClassLoader bootStrapClassLoader = extClassLoader.getParent();System.out.println(bootStrapClassLoader); //null//String使用的是引导类加载器    Java核心类库都是使用引导类加载器加载的。ClassLoader stringClassLoader = String.class.getClassLoader();System.out.println(stringClassLoader); //null}}
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、javax、sun等开头的类。

2、拓展类加载器【Extension ClassLoader】
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类
  • 父类加载器为引导类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下、也会自动由拓展类加载器加载。
3、系统类加载器【AppClassLoader|SystemClassLoader】
  • Java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 派生于ClassLoader
  • 父类加载器为拓展类加载器
  • 它负责加载环境变量classPath或系统属性 java.class.path 指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的
package com.max.jvm;import sun.misc.Launcher;import java.net.URL;public class ClassLoaderTest2 {public static void main(String[] args) {System.out.println("---------启动类加载器---------");URL[] urLs = Launcher.getBootstrapClassPath().getURLs();for (URL ls : urLs) {System.out.println(ls.toExternalForm());}System.out.println("---------拓展类加载器---------");String property = System.getProperty("java.ext.dirs");for (String path : property.split(";")) {System.out.println(path);}}
}
4、用户自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

为什么要自定义类加载器呢

  • 隔离加载类【体现在兼容中间件上】
  • 修改类的加载方式【避免同名冲突】
  • 拓展加载源
  • 防止源码泄露
2.4、关于ClassLoader

ClassLoader类、他是一个抽象类,其后面所有的类加载器都继承自ClassLoader (不含引导类加载器)

2.5、双亲委派机制

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

  • 工作原理

    • 如果一个类加载器收到了类加载请求,它并不会自己先去加载。而是把这个请求委托给它的父类加载器去执行。
    • 如果父类加载器还存在其父类加载器,则进一步向上委托。一次递归。请求最终将会达到顶层的引导类加载器
    • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子类加载器才会尝试完成加载。这就是双亲委派模式

//代码验证  新建 java.lang.String类package java.lang;public class String {public static void main(String[] args) {System.out.println("测试双亲委派");}}

String类默认交给引导类加载器去处理,而引导类只加载系统提供的类、那么系统中的String类是没有main方法的。所以报错信息。侧面体现了双亲委培机制。

双亲委培机制另一种应用说明

我们使用到了SPI接口、那么它使用了核心层api。利用双亲委派机制逆向到引导类加载器。加载核心rt.jar包。但是rt.jar中spi接口调用实现类中的方法。该方法又调用第三方的jar包,这里是jdbc.jar。第三方jar包不属于核心层的api,那么不属于引导类加载器处理。则利用双亲委派机制做一个委派,使用系统加载器处理spi具体实现类。

简单来说:接口是由引导类加载器处理、具体实现类由系统类加载器处理。

2.6、双亲委派机制优点

①、避免类的重复加载

②、保护程序安全,防止核心API被随意篡改

​ 自定义核心类库是无效的。

2.7、沙箱安全机制

​ 自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java \lang\string.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制

三、运行时数据区概述及线程

JDK1.8之后数据区。【在下面详细介绍】

3.1、内存的概念

​ 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略。保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规划,来探讨一下经典的JVM内存布局。

3.2、数据区内部线程所属

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

灰色的为单独线程私有的,红色的为多个线程共享的。即

每个线程:独立包括程序计数器、栈、本地栈

线程间共享:堆、堆外内存(永久代或元空间、代码缓存)

我们所谓的优化基本都在处理Heap与Method Area.

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

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

    当一个Java线程准备好执行以后,此时一个操作系统的本地线程也会同时创建,Java线程执行终止后,本地线程也会回收。

  • 操作系统负责所有的线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run方法。

  • 如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main (string [ ])的main线程以及所有这个main线程自己创建的线程。

  • 这些主要的后台系统线程在Hotspot JVM里主要是以下几个:

    • 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。
    • 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行,
    • GC线程:这种线程对于在JVM里不同种类的垃圾收集行为提供了支持 【比较常见的守护线程 】
    • 编译线程:这种线程在运行时、会将字节码编译到本地代码。
    • 信号调度线程:这种线程接受信号并发送给JVM、在它内部通过调用适当的方法进行处理。

四、程序计时器 (PC寄存器)

4.1、简介
-    JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能运行。
-   这里的寄存器、并非广义上所指的物理寄存器,或许将其翻译为PC计数器会桁架贴切(也称程序钩子),并且也不容易引起不必要的误会。**JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟**。
  • 作用:

​ PC寄存器用来存储指向下一条指令的地址。也就是将要执行的指令代码。由执行引擎读取下一条指令。它是一块很小的内存空间。几乎可以忽略不计,也是运行速度最快的存储区域。在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的。生命周期与线程的生命周期保持一致。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指定地址。或者、如果在执行native方法,则是未指定值(undefined)【本地方法一般为其他语言 例如C 当执行本地方法执行不出来则显示undefined】

  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计算器来完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemortError情况的区域。

4.2、区域分析

【左侧红色区域】指定地址【偏移地址】PC寄存器主要存储的结构

【右侧黄色区域】操作指令

执行引擎操作PC寄存器存储的下一个指令地址,取出操作指令。

4.3、常见问题

①、使用PC寄存器存储字节码指令地址有什么用呢?| 为什么使用PC寄存器记录当前线程执行地址呢?

答:因为CPU需要不停的切换各个线程、这时候切换回来以后,就得知道接着从哪开始继续执行。

JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

②、PC寄存器为什么会被设定为线程私有?

答:CPU经常进行线程切换、如果设为公有,那么没办法准确的知道线程从哪个地址开始执行。

为了能够准确的记录各个线程正在执行的当前字节码指令地址。最好的办法自然是为每个线程都分配一个PC寄存器,这样一来各个线程之间便可独立计算,不会互相干扰。

4.4、区分并行、并发、串行

并行和串行是相对概念。 当数据同时被处理时、那么就是并行处理。如果数据需要按一定顺序,依次执行那么就是串行。

并发则是CPU处理多个资源时、是快速的进行切换式处理。在执行的只有一个数据。

五、虚拟机栈

5.1、虚拟机栈出现的背景

由于跨平台型的设计、Java的指令都是根据栈来设计的。不同平台CPU架构不同、所以不能设计为基于寄存器的。

优点是跨平台,指令集小,编译器容易实现,缺点是性能下降。实现同样的功能需要更多的指令。

5.2、内存中的栈与堆

栈是运行时的单位、而堆是存储的单位

栈解决程序的运行问题、即程序如何执行、或者说如何处理数据,堆解决的是数据存储的问题,即数据怎么放、放在哪里.

5.3、简介
  • Java虚拟机栈是什么

Java虚拟机栈(Java Virtual Machine Stack) 早期叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame)。对应着一次次的Java方法调用。

它的生命周期和线程一致,作用是主管Java程序的运行,他保存方法的局部变量(8种基本数据变量、对象的引用地址)、部分结果。并参与方法的调用和返回。

  • 栈的特点

    • 栈是一种快速有效的分配存储的方式、访问速度仅次于程序计数器
    • JVM直接堆Java栈操作只有两个
      • 每个方法执行、伴随着进栈【push】
      • 执行结束后出栈工作【pop】
    • 对于栈来说不存在垃圾回收问题【但存在栈溢出】
5.4、栈可能出现异常情况

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

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

我们可以使用参数 -Xss 选项来设置线程的最大栈空间。栈的大小直接决定函数调用的最大可达深度。【实际经验 开发分布式项目中如果哪个模块内存占用率比较低、可以进行设置,优化整个项目的启动速度】

5.6、栈的储存单位
  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。

  • 在这个线程上正在执行的每个方法都各自对应着一个栈帧(Stack Frame)

  • 栈帧是一个内存区块、是一个数据集、维系着方法执行过程中的各类数据信息。

5.7、栈运行原理
  • JVM直接对Java栈的操作只有两个、就是对栈帧的压栈和出栈。遵循"先进后出的、后进先出"的原则。
  • 在一条活动线程中、一个时间点上,只有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被成为当前栈帧(Current Frame)。与当前栈帧相对应的方法就是当前方法(Current Method) 定义这个方法的类就是当前类(Current Class)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法、对应的新的栈帧会被创建出来,放在栈的顶端、成为新的当前帧。

  • 不用线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
  • 如果当前方法调用了其他方法、方法返回之际、当前栈帧会传回此方法的执行结果,给前一个栈帧。接着、虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
  • Java方法有两种返回函数的方式,一种是正常的函数返回。使用return指令。另外一种是抛出异常。不管使用那种方式、都会导致栈帧被弹出。
5.8、栈帧的内部结构

每个栈帧中存储着:

  • 局部变量表【Local Variables】

  • 操作数栈【Operand Stack】(或表达式栈)

  • 动态链接【Dynamic Linking】(或指向运行时常量池的方法引用)

  • 方法返回地址【Return Address】(或方法正常退出或者异常退出的定义)

  • 一些附加信息。

1、局部变量表【Local Variables】
  • 局部变量表也被称为局部变量数组或者本地变量表
  • 定义为一个数字数组【一维】、主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returenAddress类型。
  • 由于局部变量表时建立在线程的栈上的,是线程的私有数据,因此不 存在数据的安全问题
  • 局部变量表所需的容量大小是在编译器确定下来的、并保存在方法的Code属性的maximum local variables数据项中、在方法运行期间是不会改变局部变量表的大小的。
  • **方法嵌套调用的次数由栈的大小决定、一般来说、栈越大、方法嵌套调用次数越多。**对一个函数而言,它的参数和局部变量越多、使得局部变量表膨胀,它的栈帧就会越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致嵌套调用次数就会减少。
  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时、虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后、随着方法栈帧的销毁,局部变量表也会随之销毁。

【软件使用的是IDEA里面的插件 jclasslib Bytecode Viewer】在View 下 Show Bytecode With Jclasslib 即可查看。

2、关于Slot的理解
  • 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
  • 局部变量表,最基本的存储单元是 Slot【变量槽】
  • 局部变量表中存放编译器可知的各种基本类型数据(8种)、引用类型、returnAddress类型的变量
  • 在局部变量表里、32位以内的类型只占用一个Slot,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处,其余的参数按照参数表顺序继续排列。
  • 【Slot的重复利用】
    • 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

3、操作数栈【Operand stack】

栈可以使用数组或者链表进行实现。

每个独立的栈帧中除了包含局部变量表外、还包含一个先进后出的操作数栈。

操作数栈、在方法执行过程中。根据字节码指令,往栈里面写入数据或者提取数据【出栈、入栈】

操作数栈、主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新Pc寄存器中下一条需要执行的字节码指令。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
  • 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

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


每个操作数栈都会拥有一个明确的栈深度用于存储数值、其所需的最大深度在编译器就定义好了,保存在方法的Code属性中。为Max_stack的值。

栈中的任何一个元素都是可以任意的Java数据类型

  • 32bit的类型占用一个栈单位深度
  • 64bit的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈( pop)操作来完成一次数据访问。

4、代码追踪

以简单的操作来大致介绍过程【以小见大】

简要分析:PC寄存器保存指令地址。操作指令中 b代表它的数据类型也就是byte,在后续操作过程中byte、short、char、boolean都会转化为int进行保存。 执行push操作将15压栈。不过在调用方法之前操作数栈是空的。

简要分析: 这时执行下一操作、将栈顶数据弹栈存入局部变量表中。【0的索引位置被this占】

简要分析:这时执行iload指令 【因为数据类型已经转换为int了 所以是i】 从局部变量表中拿出对应位置的数值放入栈中。

简要分析:从栈中去除两个数据,出栈执行iadd操作。通过执行引擎翻译成机器指令给CPU进行处理运算。保存到索引为3的位置。 【这里存在一个点: 不管你方法有没有返回值最后的操作一定return。 所以我们Java方法返回值是void其实也是默认有返回的。】,从始至终、栈的最大使用度就是两个空间。所以最大深度就为2.

5、动态链接 【或指向运行时常量池的方法引用】

​ 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令

​ 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

通俗的理解就是

下图为常量池。

【#2代表符号引用 那么通过#2与调用方法进行一个绑定。按地址调用。】

为什么需要常量池

常量池的作用、就是为了提供一些符号和常量。便于指令的识别。

6、方法返回地址

方法返回地址、动态链接、附加一些有时被称为帧数据区

作用:存放调用该方法的PC寄存器的值。

一个方法的结束、有两种方式:

  • 正常执行完成
  • 出现未处理的异常、非正常退出。

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

​ 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。一个方法在正常调用完成之后究竟需要使用那一返回指令还需要根据方法返回值的实际数据类型而定。

​ 在字节码指令中,返回指令包含ireturn (当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。

7、附加信息【不确定存在】

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。

5.9、栈顶缓存技术【Top-Of-Stack-Cashing】技术。

​ 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack Cashing)技术,将栈顶元素全部缓存在物理CPu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。【寄存器的优势在于指定少、执行速度快。缓存到CPU寄存器中可跳过执行引擎翻译成机器指令的过程。】

5.10、方法的调用

​ 在JVM中、将符号引用转换为调用方法的直接引用与方法的绑定机制相关。

  • 静态链接

​ 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。

  • 动态链接

​ 如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。


对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
  • 早期绑定

​ 早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

  • 晚期绑定

​ 如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。


​ Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于c++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

  • 非虚方法:

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

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

  • 普通调用指令

    • invokestatic:调用静态方法、解析阶段确定唯一方法版本
    • invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
    • invokevirtual:调用所有虚方法
    • invokeinterface:调用接口方法
  • 动态调用指令
    • invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为千预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的( final修饰的除外)称为虚方法。

5.11、虚拟机栈的五道面试题
1、举例栈溢出的情况 【StackOverflowError】
  • 主函数中循环调用main函数。【详情见 5.5、设置栈内存大小】
2、调整栈大小、就能保证不出现溢出么?

不能保证。只能调整上下限。

3、分配的栈内存越大越好么?

并不是。可能会避免过早栈溢出、但不能完全避免栈溢出。而且,对性能要求会有显著的提高。在分布式项目中有比较明显的感受。 所有模块都设置很大的栈内存。那么势必整个项目的启动会随之被影响。可能会挤占其他内存空间。【辩证唯物主义论】

4、垃圾回收是否涉及到虚拟机栈?

不会的。

分析运行时数据区

1.程序计数器 既不涉及Error、也不涉及GC。只是单纯存储作用。

2.虚拟机栈 存在Error (StackOverflowError) 不存在GC。基本功能体现在入栈出栈。就算有垃圾也随着栈的操作消失。

3.本地方法栈 同虚拟机栈。

4.方法区 存在Error和GC (后续分析)

5. 存在Error和GC (后续分析)

5、方法中定义的局部变量是否线程安全?

【先理解什么是线程安全】

如果只有一个线程才可以操作此数据、则线程是安全的。

如果有多个线程操作数据、则此数据是共享数据。不考虑同步机制的话,会存在线程安全问题 。

具体问题具体分析

如果只有一个线程才可以操作此数据、则线程是安全的。例如创建一个方法、方法的内部调用没有被其他线程调用或者抢占。那么是线程安全的。但如果作为参数传递进方法。那么则有可能线程不安全。因为声明在外部可能存在多次调用。

六、本地方法接口

6.1、所处位置

6.2、什么是本地方法?

​ 一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现。比如C语言。这个特征并非Java所特有,很多其他的变成语言都有这样一个机制。比如在C++中,你可以用extern ‘C’ 告知C++编译器区调用一个C的函数。

​ 在定义一个native method时,并不提供实现体(有些像定义一个Java interface) ,因为其实现体是由非java语言在外面实现的。本地接口的作用是融合不同编程语言为Java所用。它的初衷是融合C/C++程序。

【可以自定义本地方法、但是不能和abstract连用】

6.3、为什么使用本地方法

在底层效率上C等语言具有相当大的优势。

  • 与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实现的,但是它实现调用的是该类里的本地方法setPriorityo()。这个本地方法是用c实现的,并被植入JVM内部,在windows 95的平台上,这个本地方法最终将调用win32 setPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库
(external dynamic link library)提供,然后被JVM调用。

6.4、现状

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

七、本地方法栈

Java虚拟机用于管理Java方法的调用、而本地方法栈用于管理本地方法的调用。

  • 本地方法栈也是线程私有的。
  • 允许被实现成固定或者是可动态拓展的内存大小。
  • 本地方法是使用C语言实现的。
  • 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
  • 当某个线程调用一个本地方式,它就进入了一个全新的并且不在受虚拟机限制的世界。它和虚拟机拥有同样的权限。

并不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法、也可以无需实现本地方法栈。

在Hotspot JVM中、直接将本地方法栈和虚拟机栈合二为一。

八、堆 (Heap)【重点】

8.1、堆的核心概述
  • 一个JVM实例只存在一个堆内存、堆也是Java内存管理的核心区域。

进行验证JVM是否与堆内存一一对应。

设置两个线程。分别设置他们最大最小堆空间。

-Xms10m -Xmx10m
-Xms20m -Xmx20m

在JDK的bin包中找到 jvisualvm.exe 执行.

另外一种查看方式 IDEA里插件里安装VisualGC 重新运行IDEA从右下角可以看到查看窗口。

  • Java堆区在JVM启动的时候即被创建、其空间大小也就确定了。是JVM管理的最大一块内存空间。【堆内存的大小是可以调节的】

  • 《Java虚拟机规范》规定、堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。其中对堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。【其实也有个例】

  • 所有的线程共享Java堆、在这里还可以划分线程私有的缓冲区 (Thread Local Allocation Buffer TLAB) 【间接提高了并发性】

  • 数组和对象永远不会存储在栈上、因为栈帧中保存引用。这个引用指向对象或者数组在堆中的位置。

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

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

8.2、栈堆方法区关系

8.3、内存细分

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

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

  • Young Generation Space 新生区 Young/New 【又被划分为Eden区和Survivor区】
  • Tenure Generation Space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm

Java8及之后堆内存逻辑上分为三部分: 新生区+养老区+元空间

  • Young Generation Space 新生区 Young/New 【又被划分为Eden区和Survivor区】
  • Tenure Generation Space 养老区 Old/Tenure
  • Meta Space 元空间 Meta

约定:新生区新生代年轻代 养老区老年区老年代 永久区==永久代

8.4、设置堆内存大小与OOM

Java堆区用于存储Java对象实例、那么堆的大小在JVM启动时已经设定好了、大家可以通过选项-Xmx-Xms来进行设置。

-Xmx用于表示堆区【指的是新生代+老年代】的起始内存 等价于-XX:InitialHeapSize

-Xms用于表示堆区的最大内存 等价于-XX:MaxHeapSize

  • 一旦堆区中的内存大小超过 -Xmx所设定的最大内存时、将会抛出OutOfMemoryError异常。
  • 通常会将-Xms-Xmx俩个参数配置相同的值,其目的是为了能够在java垃圾回收机制清完堆区后不需要重新分隔计算堆区大小,从而提高性能。
  • 默认情况下,初始内存大小:物理电脑内存大小/64. 最大内存大小: 物理电脑内存大小/4
package com.max.heap;public class HeapSpaceInitial {public static void main(String[] args) {//返回Java虚拟机中的堆内存总量long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;//返回Java虚拟机试图使用的最大堆内存量long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println("-Xms : " + initialMemory + "M");System.out.println("-Xmx : " + maxMemory + "M");System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}
}

  • 查看设置的参数

    • 方式一:命令行执行jsp / jstata -gc 进程id

    分析

    OC 代表新生代总量 OU 代表老年代使用率

    EC代表伊甸园区总量 EU代表伊甸园区使用量

    S0 S1代表Survivor0区 Survivor1区 他俩只能二选一进行存储使用。【垃圾回收的复制算法】

    • 方式二: 配置-XX:PrintGCDetails

8.5、新生代与老年代
  • 存储在JVM中的Java对象可以被划分为两类:

    • 一类是声明周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
    • 另一类对象的声明周期比较长、在某些极端的情况下还能够与JVM的声明周期保持一致。
  • Java堆区进一步细分的话、可以划分为年轻代和老年代。
  • 年轻代又可以划分为Eden空间。Survivor0空间和Survivor1空间(有时也叫做From区、To区)

  • 配置新生代与老年代在堆结构的占比(一般开发中不会调)

    • 默认-XX :NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
    • -XX :NewRatio=4 则表示新生代占1、老年代占4、新生代占整个堆的1/5

  • 在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1当然开发人员可以通过选项“-xx:survivorRatio”调整这个空间比例。比如-XX:SurvivorRatio=8
  • 几乎所有的Java对象都是在Eden区被new出来的
  • 绝大部分的Java对象的销毁都在新生代进行了。
    • IBM公司专门研究表明、新生代中80%的对象都是朝生夕死
    • -Xmn设置新生代最大内存大小

8.6、图解对象分配过程

对象进行分配、首先分配到伊甸园区、伊甸园区对对象进行处理。利用YGC/Minor GC淘汰失去引用的对象【图中红色显示部分】。对象正常则放入S0区【从伊甸园区转移到Survivor区默认转移到To区】。这时把对象对应的age进行+1.

当伊甸园区有新的对象进入,且充满时、则继续利用YGC或者称为Minor GC淘汰垃圾对象。这时对象是从伊甸园区转移到Survivor1区【因为S1在未转移前是空区、也称To区。S0存在对象它就是From区。 转移后那么S0则为To区】转移的过程中对应对象age递增。

​ 循环执行上述操作。当对象age达到阈值15【也可以通过设置一XX:MaxTenuringThreshold 改变阈值。】对象通过晋升(Promotion)到老年代。

总结

针对幸存者s0、s1区的总结:复制之后有交换、谁空谁是to

关于垃圾回收: 频繁在新生代收集、很少在老年代收集、几乎不在永久区/元空间收集。

8.7、对象分配的特殊情况

8.8、常见调优工具

JDK命令行

Eclipse:Memory Analyzer Tool

Jconsole

VisualVM

Jprofiler

Java Flight Recorder

GCViewer

GC Easy

软件链接

8.9、Minor GC、Major GC 、Full GC

JVM在进行GC时、并非每次都对上面三个内存(新生代、老年代、方法区)区域一起回收的、大部分时候回收的都是指新生代。

针对HotSpot VM实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集:不是完整收集整个Java堆的垃圾收集,其中分为

    • 新生代收集(Minor GC/ Young GC) : 只是新生代(Eden 、S0、S1)的垃圾收集

    • 老年代收集(Major GC / Old GC) : 只是老年代的垃圾收集

      • 目前、只有CMS GC会有单独收集老年代的行为
      • 很多时候Major GC会和Full GC混合使用,需要具体分辨是老年代回收还是整堆回收。
    • 混合收集(Mixed GC): 收集整个新生代以及部分老年代的垃圾收集

      • 目前,只有G1 GC会有这种行为。
  • 整堆收集(Full GC) : 收集整个java堆和方法区的垃圾收集。

年轻代GC(Minor GC)触发机制

当年轻代空间不足时,就会触发Minor Gc,这里的年轻代满指的是Eden代满,Survivor满不会引发Gc。(每次Minor GC会清理年轻代的内存。)

因为Java对象大多都具备朝生夕灭的特性,所以 Minor Gc非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。

Minor GC会引发STW【Stop The World】,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

老年代GC触发机制

指发生在老年代的Gc,对象从老年代消失时,我们说“Major GC”或“Full Gc发生了。

出现了Major Gc,经常会伴随至少一次的Minor Gc(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major Gc的策略选择过程)。

Major Gc的速度一般会比Minor Gc慢10倍以上,STW的时间更长。

如果Major Gc后,内存还不足,就报OOM了。

8.10、堆空间分代思想

为什么需要把Java堆分代?不分代就不能正常工作了么?

  • 经研究、不同对象的生命周期不同。70%-99%的对象是临时对象

其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

8.11、内存分配策略【对象提升Promotion规则】

原则如下:

  • 优先分配到Eden
  • 大对象直接分配到老年区
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果survivor区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄。
  • 空间分配担保

8.12、对象分配过程 :TLAB (Thread Local Allocation Buffer)

  • 堆区是线程共享区域、任何线程都可以访问到堆区种的共享数据
  • 由于对象实例的创建JVM中非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址、需要使用加锁等机制,进而影响速度。

什么是TLAB

  • 从内存模型而不是垃圾收集角度、对Eden区域进行继续划分、JVM为每个线程分配了一个私有缓存区域、它包含在Eden空间内。
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
  • 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
  • 在程序中,开发人员可以通过选项“-xx:UseTLAB”设置是否开启TLAB空间。
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

具体分析

当字节码文件加载进来后、创建对象。先将这个线程对应对象的内存分配到TLAB里。如果能正常存储、则继续下面操作、既保证了线程安全、又确保了新能。 如果超过TLAB的大小。那么将正常保存Eden区域。多线程的区分则是通过锁机制处理。

8.12、小结堆空间的参数设置
* 测试堆空间常用的Jvm参数* -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值* -XX:+PrintFlagsFinal  :查看所有的参数的最终值(修改后的值,不再是初始值)* 具体查看某个参数的指令: jps:查看当前运行中的进程 ->  jinfo -flag SurvivorRatio 进程id* -Xms:初始堆空间内存 (默认为物理内存的1/64)* -Xmx:最大堆空间内存(默认为物理内存的1/4)* -Xmn:设置新生代的大小。(初始值及最大值)* -XX:NewRatio:配置新生代与老年代在堆结构的占比 (1:2)* -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例 (8:1:1)* -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄* -XX:+PrintGCDetails:输出详细的GC处理日志* 打印gc简要信息:① -XX:+PrintGC   ② -verbose:gc* -XX:HandlePromotionFailure:是否设置空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。

  • 如果大于,则此次Minor Gc是安全的
  • 如果小于、则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
    • 如果-XX:HandlePromotionFailure=true。那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。

      • 如果大于、则尝试进行一次Minor GC,但这次Minor GC依旧是有风险的。
      • 如果小于,则改为进行一次Full GC。
    • 如果HandlePromotionFailure=false 则改为进行一次Full GC
拓展:堆是分配对象存储的唯一选择么?

在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术

此外,前面提到的基于openJDK深度定制的TaoBaoVM,其中创新的GCIH (GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且Gc不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

  • 如何将堆上的对象分配到栈、需要使用逃逸分析手段。
  • 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
  • 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
  • 逃逸分析的基本行为就是分析对象动态作用域
    • 当一个对象在方法中被定义后、对象只在方法内部使用。则认为没有发生逃逸
    • 当一个对象在方法中被定义后、它被外部方法所引用。则认为发生逃逸。例如作为调用参数传递到其他地方。

虽然逃逸分析在内存管理上是优化操作、但它还并不成熟。其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。 且它只能在服务器端开启。

九、方法区【重点】

9.1、栈、堆、方法区的交互关系
整体数据区情况

9.2、方法区的理解

《Java虚拟机规范》中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。” 但对于Hot Spot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

所以,方法区看作是一块独立于Java堆的内存空间

  • 方法区(Method Area)与ava堆一样,是各个线程共享的内存区域。

  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.outOfMemoryError:PermGen space 或者 java.lang.outofMemoryError: Metaspace 【加载了大量的第三方的jar包可能会导致 】

  • 关闭JVM就会释放这个区域的内存。

  • 在jdk7及以前、习惯上把方法区,称为永久代【PermGen Spacne】。Jdk8开始、使用元空间取代了永久代。

  • 本质上、方法区和永久代并不等价。仅是对hotspot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit / IBM J9 中不存在永久代的概念。

永久代主要略势在于它占用的是虚拟机内存。且容易OOM。

  • 而到了JDK8、终于完全废弃了永久代的概念。改用了JRockit、J9一样在本地内存中实现的元空间Metaspace来代替。
  • 元空间的本质和永久代类似。都是对JVM规范中的方法区的实现。它与永久区最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
  • 永久代、元空间二者并不只是名字变了、内部结构也调整了。
9.3、设置方法区大小与OOM
  • JDK 1.7情况下方法区大小 【方法区的大小不必是固定的、JVM可以根据应用的需要动态调整】

通过-xx:PermSize来设置永久代初始分配空间。默认值是20.75M

-XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M

当JVM加载的类信息容量超过了这个值,会报异常outOfMemoryError:PermGenspace

  • JDK 1.8及以后

元数据区大小可以使用参数**-XX:Metaspacesize-XX:MaxMetaspacesize**指定,替代上述原有的两个参数。

默认值依赖于平台。windows下,-XX :Metaspacesize是21M,-XX:MaxMetaspacesize的值是-1,即没有限制。

与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常outOfMemoryError: Metaspace

-XX:Metaspacesize:设置初始的元空间大小。对于一个64位的服务器端JTVM来说,其默认的-XX:Metaspacesize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于Gc后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。

如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full Gc多次调用。为了避免频繁地cC ,建议将-xx:Metaspacesize设置为一个相对较高的值

9.4、如何解决这些OOM 【在后面具体讲。此处简述】

方法区溢出、主要体现在类的过多创建。超过设置默认的阈值。

  • 1、要解决oOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow).

    内存泄漏

    简单理解就是存在堆的中数据已经不在需要。但还是存在栈或者其他方式的引用仍然存在。GC无法回收这些数据。过多则会引起泄漏。

  • 2、如果是内存泄漏,可进一步通过工具查看泄漏对象到Gc Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。

  • 3、如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序
    运行期的内存消耗。

9.5、方法区的内部结构

《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:

它用于存储已被虚拟机加载的类型信息常量静态变量即时编译器(JIT)编译后的代码缓存等。

1、类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  • 这个类型的完整有效名称(全名=包名.类名)
  • 这个类型直接父类的完整有效名(对于interface或是java.lang.0bject,都没有父类)
  • 这个类型的修饰符(public,abstract,final的某个子集)
  • 这个类型直接接口的一个有序列表
2、域信息/属性信息
  • JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
  • 域的相关信息包括:域名称、域类型、域修饰符(public, private,protected, static,final, volatile, transient的某个子集)
3、方法的信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  • 方法名称
  • 方法的返回类型(或 void)
  • 方法参数的数量和类型(按顺序)
  • 方法的修饰符 (public、private、protected、static、final、native、synchronized、abstract的一个子集)
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
  • 异常表
//测试一下加载前的class信息
package com.max.method;import java.io.Serializable;
//单继承Object类、多实现序列化还有比较器
public class MethodsTest extends Object implements Serializable,Comparable {//属性public int num = 0;private static String str = "Hello World";//默认的构造器//方法public void  test (){System.out.println("我是方法一");}public static String test2(){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}return str;}public static void main(String[] args) {MethodsTest methodsTest = new MethodsTest();}@Overridepublic int compareTo(Object o) {return 0;}
}

通过反编译生成一个txt文件。 javap -v MethodsTest.class > Method.txt

Classfile /E:/Study/JVM/jvm/method/target/classes/com/max/method/MethodsTest.classLast modified 2022年5月15日; size 1334 bytesMD5 checksum 3fe2e2e96a24e460193b7dae0a4cc240Compiled from "MethodsTest.java"//class类的继承实现情况
public class com.max.method.MethodsTest implements java.io.Serializable,java.lang.Comparableminor version: 0major version: 52flags: (0x0021) ACC_PUBLIC, ACC_SUPER   // 修饰符this_class: #12                         // com/max/method/MethodsTestsuper_class: #15                        // java/lang/Objectinterfaces: 2, fields: 2, methods: 6, attributes: 1
Constant pool:#1 = Methodref          #15.#48        // java/lang/Object."<init>":()V#2 = Fieldref           #12.#49        // com/max/method/MethodsTest.num:I#3 = Fieldref           #50.#51        // java/lang/System.out:Ljava/io/PrintStream;#4 = String             #52            // 我是方法一#5 = Methodref          #53.#54        // java/io/PrintStream.println:(Ljava/lang/String;)V#6 = Long               10l#8 = Methodref          #55.#56        // java/lang/Thread.sleep:(J)V#9 = Class              #57            // java/lang/InterruptedException#10 = Methodref          #9.#58         // java/lang/InterruptedException.printStackTrace:()V#11 = Fieldref           #12.#59        // com/max/method/MethodsTest.str:Ljava/lang/String;#12 = Class              #60            // com/max/method/MethodsTest#13 = Methodref          #12.#48        // com/max/method/MethodsTest."<init>":()V#14 = String             #61            // Hello World#15 = Class              #62            // java/lang/Object#16 = Class              #63            // java/io/Serializable#17 = Class              #64            // java/lang/Comparable#18 = Utf8               num#19 = Utf8               I#20 = Utf8               str#21 = Utf8               Ljava/lang/String;#22 = Utf8               <init>#23 = Utf8               ()V#24 = Utf8               Code#25 = Utf8               LineNumberTable#26 = Utf8               LocalVariableTable#27 = Utf8               this#28 = Utf8               Lcom/max/method/MethodsTest;#29 = Utf8               test#30 = Utf8               test2#31 = Utf8               ()Ljava/lang/String;#32 = Utf8               e#33 = Utf8               Ljava/lang/InterruptedException;#34 = Utf8               StackMapTable#35 = Class              #57            // java/lang/InterruptedException#36 = Utf8               main#37 = Utf8               ([Ljava/lang/String;)V#38 = Utf8               args#39 = Utf8               [Ljava/lang/String;#40 = Utf8               methodsTest#41 = Utf8               compareTo#42 = Utf8               (Ljava/lang/Object;)I#43 = Utf8               o#44 = Utf8               Ljava/lang/Object;#45 = Utf8               <clinit>#46 = Utf8               SourceFile#47 = Utf8               MethodsTest.java#48 = NameAndType        #22:#23        // "<init>":()V#49 = NameAndType        #18:#19        // num:I#50 = Class              #65            // java/lang/System#51 = NameAndType        #66:#67        // out:Ljava/io/PrintStream;#52 = Utf8               我是方法一#53 = Class              #68            // java/io/PrintStream#54 = NameAndType        #69:#70        // println:(Ljava/lang/String;)V#55 = Class              #71            // java/lang/Thread#56 = NameAndType        #72:#73        // sleep:(J)V#57 = Utf8               java/lang/InterruptedException#58 = NameAndType        #74:#23        // printStackTrace:()V#59 = NameAndType        #20:#21        // str:Ljava/lang/String;#60 = Utf8               com/max/method/MethodsTest#61 = Utf8               Hello World#62 = Utf8               java/lang/Object#63 = Utf8               java/io/Serializable#64 = Utf8               java/lang/Comparable#65 = Utf8               java/lang/System#66 = Utf8               out#67 = Utf8               Ljava/io/PrintStream;#68 = Utf8               java/io/PrintStream#69 = Utf8               println#70 = Utf8               (Ljava/lang/String;)V#71 = Utf8               java/lang/Thread#72 = Utf8               sleep#73 = Utf8               (J)V#74 = Utf8               printStackTrace
{public int num;descriptor: I                   //返回值flags: (0x0001) ACC_PUBLIC     //修饰符public com.max.method.MethodsTest();   //构造器descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_05: iconst_06: putfield      #2                  // Field num:I9: returnLineNumberTable:line 5: 0line 8: 4LocalVariableTable:Start  Length  Slot  Name   Signature0      10     0  this   Lcom/max/method/MethodsTest;public void test();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=2, locals=1, args_size=1          //栈深度、变量数、参数 这里是this0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #4                  // String 我是方法一5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 15: 0line 16: 8LocalVariableTable:Start  Length  Slot  Name   Signature0       9     0  this   Lcom/max/method/MethodsTest;public static java.lang.String test2();descriptor: ()Ljava/lang/String;flags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=00: ldc2_w        #6                  // long 10l3: invokestatic  #8                  // Method java/lang/Thread.sleep:(J)V6: goto          149: astore_010: aload_011: invokevirtual #10                 // Method java/lang/InterruptedException.printStackTrace:()V14: getstatic     #11                 // Field str:Ljava/lang/String;17: areturnException table:from    to  target type0     6     9   Class java/lang/InterruptedExceptionLineNumberTable:line 21: 0line 24: 6line 22: 9line 23: 10line 26: 14LocalVariableTable:Start  Length  Slot  Name   Signature10       4     0     e   Ljava/lang/InterruptedException;StackMapTable: number_of_entries = 2frame_type = 73 /* same_locals_1_stack_item */stack = [ class java/lang/InterruptedException ]frame_type = 4 /* same */public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=2, args_size=10: new           #12                 // class com/max/method/MethodsTest3: dup4: invokespecial #13                 // Method "<init>":()V7: astore_18: returnLineNumberTable:line 31: 0line 33: 8LocalVariableTable:Start  Length  Slot  Name   Signature0       9     0  args   [Ljava/lang/String;8       1     1 methodsTest   Lcom/max/method/MethodsTest;public int compareTo(java.lang.Object);descriptor: (Ljava/lang/Object;)Iflags: (0x0001) ACC_PUBLICCode: stack=1, locals=2, args_size=20: iconst_01: ireturnLineNumberTable:line 37: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       2     0  this   Lcom/max/method/MethodsTest;0       2     1     o   Ljava/lang/Object;static {};descriptor: ()Vflags: (0x0008) ACC_STATICCode:stack=1, locals=0, args_size=00: ldc           #14                 // String Hello World2: putstatic     #11                 // Field str:Ljava/lang/String;5: returnLineNumberTable:line 9: 0
}
SourceFile: "MethodsTest.java"
4、non-final的类变量
package com.max.method;public class MethodsTest2 {public int num = 0;public static final int numsf = 1;public static  int nums = 2;public final int  numf = 3;public static void main(String[] args) {//测试final修饰变量在编译期是否已经对其进行赋值}}
  public int num;descriptor: Iflags: (0x0001) ACC_PUBLICpublic static final int numsf;descriptor: Iflags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINALConstantValue: int 1public static int nums;descriptor: Iflags: (0x0009) ACC_PUBLIC, ACC_STATICpublic final int numf;descriptor: Iflags: (0x0011) ACC_PUBLIC, ACC_FINALConstantValue: int 3

显然 final声明。初始化数值是在编译期完成的。

静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。

类变量被类的所有实例共享,即使没有类实例时你也可以访问它。

5、运行时常量池
  • 方法区。内部包含了运行时常量池。
  • 字节码文件 内部包含了常量池

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外、还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引号。

为什么需要一个常量池

一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持。通常这种数据会很大以至于不能直接存到字节码里。换另一种方式。可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。

常量池中有什么

  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用

小结:

​ 常量池、可以看作是一张表。虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

  • 运行时常量池(Runtime constant Pool)是方法区的一部分。
  • 常量池表(Constant Pool Table)是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  • JVM为每个已加载的类型(类或者接口)都维护一个常量池。池中的数据项通过索引访问。
  • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
    • 运行时常量池、相对于Class文件常量池的另一重要特征是:具备动态性
  • 运行时常量池类似于传统编程语言中的符号表。
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛outofMemoryError异常。
9.6、方法区的演进细节

首先明确:只有HotSpot才有永久代

Hotspot中方法区的变化

jdk1.6之前 有永久代,静态变量存放在永久代上。
jdk1.7 有永久代、但已经逐步去永久代,字符串常量池、静态变量移除,保存在堆上。
jdk1.8及以后 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆

9.7、永久代为什么要被元空间替换
  • 随着Java8 的到来,HotSpot VM中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace ) 。

  • 由于类的元数据分配咋本地内存中、元空间的最大可分配空间就是系统可用内存空间。原因如下:

    • 为永久代设置空间大小是很难确认的。【大了浪费、小了容易OOM】

    在某些场景下、如果动态加载类过多、容易产生Perm区的OOM。比如某个实际Web工程中、因为功能点比较多、在运行过程中、要不断动态加载很多类、经常出现错误。

    元空间和永久代之间最大的区别在于:元空间并不在虚拟机中、而是使用本地内存。因此、默认情况下,元空间的大小仅仅受本地内存限制。

    • 对永久代进行调优是很困难的

​ 有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)。

​ 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的Hotspot虚拟机对此区域未完全回收而导致内存泄漏。

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量不在使用的类型。但整个类型回收是一件相当复杂的事情。

9.8、StringTable 【字符串常量池\字符串字面量】为什么要调整?

jdk7中将StringTable放在了堆空间中。因为永久代的回收效率很低、在full gc的时候才会触发。 而full gc是老年代的空间不足。永久代不足时才会触发。这就导致了StringTable回收效率不高、而我们开发中会有大量的字符串被创建、回收效率低、导致永久代内存不足。放在堆里面,能及时回收内存。

9.9、方法区的垃圾回收
  • 先来说说方法区内常量池之中主要存放的两大类常量。字面量和符号引用。字面量比较接近Java语言层次的常量概念。如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:

    • 1、类和接口的全限定名
    • 2、字段的名称和描述符
    • 3、方法的名称和描述符
  • HotSpot虚拟机对常量池的回收策略是很明确的。只要常量池中的常量没有被任何地方引用、就可以被回收。

  • 回收废弃常量与回收Java堆中的对象非常类似。


  • 判定一个常量是否’废弃’还是相对简单的。而要判定一个类型是否属于不在被使用的类的条件就比较苛刻了。需要同时满足一下三个条件,

    • 该类的所有实例都已经被回收、也就是Java堆中不存在该类以及任何派生子类的实例
    • 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景。如OSGI、JSP的重加载等。否则通常是很难达成的。
    • 该类对用的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  • Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+Traceclass-Loading、-XX:+TraceclassUnLoading查看类加载和卸载信息
  • 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及oSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
9.10、运行时数据区的常见大厂面试题。

百度

1、说一下JVM内存模型、有哪些区? 分别是干什么的

简单梳理:

程序计数器存储程序执行下一条指令的地址,也是执行速度最快的区域。

本地方法栈则是用来管理本地方法的一个区域

虚拟机栈是负责运行的单位。记录程序如何执行、如何存储的。

堆是存储的单位、存储新建对象。也是GC主要处理的区域。

方法区则是用于存储已被虚拟机加载的类型信息常量静态变量即时编译器(JIT)编译后的代码缓存等。

蚂蚁金服:

1、Java8的内存分代改进

jdk1.6之前 有永久代,静态变量存放在永久代上。
jdk1.7 有永久代、但已经逐步去永久代,字符串常量池、静态变量移除,保存在堆上。
jdk1.8及以后 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆

2、JVM内存分哪几个区、每个区的作用【 同上 略 】

3、JVM内存分布\内存结构? 栈和堆的区别? 堆的结构? 为什么两个survivor区

简述:

堆的结构:由新生代,老年代组成。新生代又分为伊甸园区、幸存者0区、幸存者1区。

4、Eden和Survior的比例分配

默认 1 : 2

小米

JVM内存分区、为什么要有新生代和老年代

简述:

1)新生代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。
2)老年代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。

字节

1、Java的内存分区

2、讲讲JVM运行时数据库区

3、什么时候对象会进入老年代。

简述:

1、当对象通过GC进行回收仍然存在、并且在在幸存者区完成age的递增,到阈值后进入老年代。

2、当GC处理的数据流向幸存者区时、幸存者区满了。那么数据会直接放到老年代。

3、当存在大对象、伊甸园区和幸存者区完全放不下,那么就会直接进入老年区。

京东

JVM的内存结构、Eden和Survivor比例

JVM内存为什么要分成新生代、老年代、持久代。新生代中为什么要分为Eden和Survivor

天猫

JVM内存模型以及分区、需要详细到每个放什么

JVM的内存模型、Java8做了什么修改

拼多多

JVM内存分为哪几个区、每个区的作用是什么

美团

1、Java内存分配

2、JVM的永久代中会发生垃圾回收么

简答:

会的、永久代中主要存放字符串常量和类型。只要常量池中的常量没有被任何地方引用、就可以被回收。对于类型回收会比较麻烦。

该类的所有实例都已经被回收、也就是Java堆中不存在该类以及任何派生子类的实例

加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景。如OSGI、JSP的重加载等。否则通常是很难达成的。

该类对用的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

这里说的仅仅是“被允许”、具体。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,

3、JVM内存分区、为什么要有新生代和老年代

十、对象的实例化内存布局与访问定位。

10.1、对象的实例化

对象创建方式

  • new创建对象是最常见的方法、其他还有Xxx的静态方法、XxxBuilder/XxxFacroty的静态方法。
  • Class的newInstance() : 反射的方式、只能调用空参的构造器,权限必须是public。
  • Constructor的newInstance(Xxx) : 反射的方式。可以调用空参、或者带参的构造器。权限没有要求。
  • clone() : 不调用任何构造器,当前类需要实现Cloneable接口,实现clone()方法
  • 使用反序列化 : 从文件中、或者网络中获取一个对象的二进制流。
  • 第三方库Objenesis。

对象创建的步骤

  • 【判断对象对应的类是否加载、链接、初始化】

虚拟机遇到一条new指令、首先区检查这个指令的参数是否在Metaspace的常量池中定位到一个类的符号引用。并且检查这个符号引用的类是否已经被加载、解析和初始化。(即判断类元信息是否存在)。如果没有那么在双亲委派模式下、使用当前类加载器以ClassLoader+包名+类名为key进行查找对应的class文件。如果没有找到文件。则抛出ClassNotFountException异常。如果找到、则进行类的加载、并生成对应Class类对象。

  • 【为对象分配空间】

首先计算对象占用空间大小、接着在堆中划分一块内存给新对象

如果实例成员变量是引用变量。仅分配引用变量空间即可,即4个字节大小。

  • 如果内存规整 【指针碰撞】

如果内存是规整的、那么虚拟机将采用的是指针碰撞法来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact(整理)过程的收集器时,使用指针碰撞。

  • 如果内存不是规整的【空闲列表】

如果内存不是规整的、已使用的内存和未使用的内存互相交错。那么虚拟机将采用空闲列表法来为对象分配内存,意思是虚拟机维护了一个列表,记录上哪些内存是可以用的、在分配的时候从列表中找到一块足够用的区域划给对象,并更新列表上的内容。这种分配方式称为空闲列表

  • 【处理并发安全问题】

    • 采用CAS失败重试、区域加锁保证更新的原子性
    • 每个线程先分配一块TLAB
  • 【初始化分配的空间】

    • 所有属性设置默认值、保证对象实例字段在不赋值时可以直接使用
  • 【设置对象的对象头】

    • 下面讲解
  • 执行Init方法进行初始化

到底new算创建对象还是 构造器初始化时才算创建了对象呢?

其实new就算是创建了一个对象框架。构造器初始化后才细化了对象的完整对象。

10.2、对象的内存布局

  • 【对象头 (Header)】 如果是数组、还需要记录数组的长度。

    • 运行时元数据 Mark Word
    • 哈希值【栈对应对象空间的索引值】
    • GC分代年龄 【新生代到老年代】
    • 锁状态标志
    • 线程持有的锁
    • 偏向线程ID
    • 偏向实践戳
    • 类型指针
      • 指向类元数据InstanceKlass、确定该对象所属的类型。
  • 【实例数据 (Instance Data)】
    • 说明 : 它是对象正常存储的有效信息、包括程序代码中定义的各种类型的字段(包括从父级继承下来的本身拥有的字段)
    • 规则
      • 相同宽度的字段总是被分配在一起
      • 父类中定义的变量会出现在子类之前
      • 如果CompactFields参数为true、子类的窄变量可能插入到父类变量的空隙。

10.3、对象访问定位

JVM是如何通过栈帧中的对象引用访问到其内部对象实例的呢?

句柄访问

  • 优点:当常量池发生变化、引用地址发生改变、那么无需改变栈到句柄池的位置。
  • 缺点:多开辟出来一个区域、且执行速度会相当对来说会慢一些

直接指针

​ Hotspot采用

4、大厂面试题

美团

1、对象在JVM中是怎么存储的

2、对象头信息里面有哪些东西

蚂蚁金服

Java对象头里面有什么

十一、直接内存 【Direct Memory】

  • 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
  • 直接内存是在Java堆外的、直接向系统申请的内存区间。
  • 来源于NIO ,通过存在堆中的DirectByteBuffer操作Native内存通常,访问直接内存的速度会优于Java堆。即读写性能高。
  • 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。Java的NIO库允许Java程序使用直接内存,用于数据缓冲区

传统文件读取写入操作过程。

JVM写入数据到物理磁盘。首先要写入用户地址空间、接着复制到内存地址空间,在写入到物理磁盘。 其间发生了用户态切换内核态。这时需要两份内存存储重复数据,效率低。

使用NIO时,如右图。操作系统划出的直接缓存区可以被java代码直接访问,只有—份。NIO 适合对大文件的读写操作。 省去了中间商赚差价。

  • 也可能导致OutOfMemoryError异常
  • 由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
  • 缺点
    • 分配回收成本较高
    • 不受JVM内存回收管理
  • 直接内存大小可以通过MaxDirectMemorysize设置
  • 如果不指定、默认与堆的最大值-Xmx参数值一致。

十二、执行引擎

12.1、执行引擎概述

执行引擎是Java虚拟机核心之一。

虚拟机是一个相对于物理机的概念。这两种机器都有代码执行能力、其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上。而虚拟机的执行引擎是由软件自行实现的。因此可以不受物理条件制约定制指令集与执行引擎的结构体系。能够执行哪些不被硬件直接支持的指令集格式。

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

12.2、执行引擎的工作过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RZQQSQvF-1652923017436)(…/AppData/Roaming/Typora/typora-user-images/image-20220517083556057.png)]

从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。

12.3、Java代码编译和执行的过程

大部分的程序代码转化为物理机的目标代码或虚拟机能执行的指令集之前、都需要上述步骤。

什么是解释器 Interpreter 、什么是JIT编译器?

解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行。将每条字节码文件中的内容翻译为对应平台的本地机器指令执行。

JIT Just In Time Compiler 编译器:在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。

JDK1.0时代。将Java语言定位为’解释执行’还是比较合适的。在后来,Java也发展出可以直接生成本地代码的编译器。

现在JVM在执行Java代码的时候、通常都会将解释执行与编译执行二者结合起来进行。

这就是为什么Java被称为半解释半编译的语言。

12.4、理解机器码、指令、汇编语言。

机器码

  • 各种用二进制编码方式表示的指令,叫做机器指令码。开始,人们就用它采编写程序,这就是机器语言。、
  • 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。
  • 用它编写的程序一经输入计算机,CPU直接读取运行,因此和其他语言编的程序相比,执行速度最快。
  • 机器指令与CPU紧密相关,所以不同种类的CPU所对应的机器指令也就不同。

指令

  • 由于机器码是有o和1组成的二进制序列,可读性实在太差,于是人们发明了指令。B指令就是把机器码中特定的o和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好
  • 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同。

指令集

  • 不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。
  • X86指令集、ARM指令集

汇编语言

  • 由于指令的可读性还是太差,于是人们又发明了汇编语言。

  • 在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(symbo1)或标号(Label)代替指令或操作数的地址。

  • 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。gaoji

高级语言

  • 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言
  • 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。

12.5、解释器

解释器工作机制

解释器真正意义上的所承担的角色就是一个运行时翻译者,将字节码文件中的内容翻译为对应平台的本地机器指令执行。

当一条字节码指令被解释执行完成后、接着在根据PC寄存器中的记录的下一条需要被执行的字节码指令执行解释过程。

在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普遍使用的模板解释器。

字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率非常低下。而模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。

现状

由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的,比如Python、Perl、Ruby等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些C/C++程序员所调侃。

为了解决这个问题,JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器吗,母次出数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。

12.6、JIT编译器【及时编译器】

HotSpot VM是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。

有些开发人员会感觉到诧异,既然Hotspot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。

首先明确:当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。编译器要想发挥作用、吧代码编译成本地代码、需要一定执行的时间。但编译为本地代码、执行效率高。所以:尽管JRockit VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。

案例

注意解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态可以承受的负载大于冷机状态。如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无承载流量而假死。

在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的1/8。曾经有这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机。此故障说明JIT的存在 —— 阿里团队

概念解释

Java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程;

热点代码及探测方式

当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。

  • 一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (on StackReplacement)编译。

  • 一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。

  • 目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。

  • 采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter) 。

    方法调用计数器用于统计方法的调用次数
    回边计数器则用于统计循环体执行的循环次数

    • 这个计数器就用于统计方法被调用的次数,它的默认阈值在clie模式下是1500 次,在 Server模式下是10000 次。超过这个阈值,就会触发JIT编译。
    • 这个阈值可以通过虚拟机参数-xx:CompileThreshold来人为设定。
    • 当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

  • 回边计数器

它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。

十三、StringTable

1、String的基本特性
  • String:字符串,使用一对‘""引起来表示。
  • String声明为final的、不可以被继承
  • String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小。
  • String在jdk8及以前内部定义final char[] value 用于存储字符串数据。jdk9改为byte[]。

结论: String再也不同char[] 来存储、改成了byte[]加上编码标记,节约了一些空间.

  • String:代表不可变的字符序列。简称:不可变性

    • 当对字符串重新赋值时、需要重写指定内存区域赋值。不能使用原来的value进行赋值。
    • 当对现有的字符串进行连接操作时、也需要重新指定内存区赋值。不能使用原有的value进行赋值。
    • 当调用String的replace()方式修改指定字符或字符串时、也需要重新指定内存区域赋值。不能使用原来的value进行赋值。
  • 通过字面量的方式给一个字符串赋值。此时的字符串值生命在字符串常量池中。

  • 字符串常量池是不会存储相同内容的字符串的。【共同引用。】

    • string的string Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。在jdk7中,stringTable的长度默认值是60013,1009是可设置的最小值。
    • 使用-XX : StringTableSize可设置StringTable的长度
2、String的内存分配

在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。

    • ​ 比如 String info = “Max”;
  • 如果不是引用双引号声明的String对象、可以使用String提供的intern()方法 【后讲解】

JDK 6 之前、字符串常量池存放在永久代。

JDK 7 中字符串常量池的位置调整到了Java堆里面

JDk 8 元空间 字符串常量池在堆里。

调整到堆中:①PermSize默认比较小 ②永久代垃圾回收频率低。

  • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
  • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7 中使用string.intern ( )。
3、String的基本操作
  • 常量与常量的拼接结果在常量池、原理是编译期优化。
  • 只要其中一个有一个是变量。结果就在堆中。变量拼接的原理是StringBuilder
  • 如果拼接的结果调用intern()方法。则主动将常量池中还没有的字符串对象放入池中。并返回此对象地址。
  • 执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式。
public class StringTest01 {public static void main(String[] args) {String s1 = "Max";String s2 = "Im";String s3 = "MaxIm";String s4 = "Max" + "Im";String s5 = s1 + "Im";String s6 = "Max" + s2;String s7 = s1 + s2;System.out.println(s3 == s4);   //编译期的优化   trueSystem.out.println(s3 == s5);   //如果拼接过程中 出现了变量  则需要在堆空间中new String()  内容为拼接之后的结果  falseSystem.out.println(s3 == s6);   //falseSystem.out.println(s3 == s7);   //falseSystem.out.println(s5 == s7);   //falseSystem.out.println(s6 == s7);   //false//intern() : 判断字符串常量池中是否存在MaxIm值、如果存在则返回常量池中MaxIm地址// 如果不存在则在常量池中加载一份MaxIm。并且返回对象地址       String s8 = s6.intern();System.out.println(s3 == s8);  //ture}
}
4、intern()的使用

如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会把字符串常量池中查询当前字符串是否存在。若不存在就会将当前字符串放入常量池中。

比如:String info = new String(“Info”).intern();

也就是说,如果在任意字符串上调用String.intern()方法。那么其返回结果所指向的那个类的实例。必须和直接以常量的形式出现的字符串实例完全相同。因此、下列表达式必定是true: (“a” + “b” +
“c”).intern() = “abc”。

通俗点讲、Interned String就是确保字符串在内存中只有一份拷贝。这样可以节约内存空间,加快字符串操作任务的执行速度。注意、这个值会被存放在字符串内部池(String Intern Pool)

new String()到底创建了几个对象。

一个对象是:new关键字在堆空间创建的。

另一个对象是:字符串常量池中的对象。

一篇文章掌握整个JVM,JVM超详细解析。【持续更新 康师傅yyds】相关推荐

  1. 一篇文章搞定GVIM(根据工作经验持续更新)

    文章目录 0.引言 1.在Linux下面安装VIM 2.基本操作 2.1三种模式 2.1 保存退出:wq没反应?! 2.2 解决鼠标不能用的问题 2.3 VIM上下左右移动hjkl 2.4 跳转到第n ...

  2. 风格迁移0-04:stylegan-论文超详细解读(持续更新修改)

    以下链接是个人关于stylegan所有见解,如有错误欢迎大家指出,我会第一时间纠正,如有兴趣可以加微信:17575010159 相互讨论技术. 风格迁移0-00:stylegan-目录-史上最全:ht ...

  3. Android技能树 — 网络小结(6)之 OkHttp超超超超超超超详细解析

    前言: 本文也做了一次标题党,哈哈,其实写的还是很水,各位原谅我O(∩_∩)O. 介于自己的网络方面知识烂的一塌糊涂,所以准备写相关网络的文章,但是考虑全部写在一篇太长了,所以分开写,希望大家能仔细看 ...

  4. 单片机数字钟(调时,调时闪烁,万年历,年月日)超详细解析

    2019/07/13 单片机数字钟(调时,调时闪烁,万年历,年月日)超详细解析 发表日期:2019/07/13 单片机开发板:巫妖王2.0, 使用同款开发板可直接上板测试 文档说明: 实现功能 : 一 ...

  5. 计算机网络之交换机的工作原理---超详细解析,谁都看得懂!!

    在了解交换机的工作原理之前,我们先要了解几个概念. 一.相关概念  1.OSI七层模型是哪七层? 自上而下分别是: 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 交换机工作在数据链路层, ...

  6. 一篇文章掌握整个JVM,JVM超详细解析!!!

    JVM 先想想一些问题 1 我们开发人员编写的Java代码是怎么让电脑认识的 2 为什么说java是跨平台语言 3 Jdk和Jre和JVM的区别 4 为什么要学习JVM 深入学习JVM 1 JVM运行 ...

  7. 超硬核!!!一篇文章掌握整个JVM,JVM超详细解析!!!

    JVM 先想想一些问题 1 我们开发人员编写的Java代码是怎么让电脑认识的 2 为什么说java是跨平台语言 3 Jdk和Jre和JVM的区别 4 为什么要学习JVM 深入学习JVM 1 JVM运行 ...

  8. 计算机网络第八版——第二章课后题答案(超详细)

    第二章 该答案为博主在网络上整理,排版不易,希望大家多多点赞支持.后续将会持续更新(可以给博主点个关注~ 第一章 答案 [2-01]物理层要解决哪些问题?物理层的主要特点是什么? 解答:物理层考虑的是 ...

  9. BERT文本分类,代码超基础、超详细解析

    声明: 关于文章: 内容:使用bert进行新闻文本分类, 目的:熟悉预训练模型的使用过程以及数据处理,和模型的各个接口,输入输出,做到对bert的简单使用 环境:windows,pytorch,tra ...

  10. Qt基础入门到进阶实战资料大全(纯干货,超详细,随时更新)

    文章目录 1.简介 2.Qt官网 3.Qt开源社区 1)国内社区 a)CSDN 社区 b)Qter社区 c)QTCN开发网 2)国外社区 a)QT software b)Qt Center c)Dev ...

最新文章

  1. Mxnet TensorRT
  2. 浅析ajax原理与用法
  3. pythonurllib微博登录怎么删_Python使用cookielib和urllib2模拟登陆新浪微博并抓取数据...
  4. js数组去重的三种常用方法
  5. openjdk和jdk_JDK 11:发行候选更新和OpenJDK JDK 11 LTS
  6. anaconda安装python3.7、jupyter_Anaconda3 Python 3 和 2 in Jupyter Notebook共存方法
  7. Python可视化工具Matplotlib 3.0版出炉,改进默认后端选择,饼图终于变圆了
  8. uniapp手写地图搜索选取功能
  9. 《C++代码设计与重用》——2.7 转型
  10. 基于Android的城市环境监测系统
  11. 快乐的实现单独页面横屏
  12. 图片处理--处理成灰色图片,高斯模糊
  13. 计算机删除内置用户,如何删除windows的内置账户,administrator
  14. 用python的re库统计《斗破苍穹》词频
  15. Linux下的启动oracle服务 启动监听 开放端口操作
  16. qspi(spi四线模式)
  17. MMS - 什么是MMS?
  18. 调用百度地图接口获取城市住宅小区边界信息 (米制转经纬度)
  19. 物联网智能家居项目总结(全)
  20. java cleartype_等宽雅黑宋体2.1(支持ClearType和GDI++)

热门文章

  1. Origin2018安装教程
  2. H3CSE园区-STP
  3. 安装Google服务包
  4. IPv4中IP地址的分类
  5. 2017百度之星资格赛总结
  6. 构建病毒宿主关系知识图谱
  7. 获取Bootcamp 6 下载地址(mac装win10)
  8. Structs2文件上传以及预览
  9. 官网下载mysql jar包
  10. 最像windows的linux系统,盘点酷似Windows的Linux发行版