mirror of
https://github.com/youthlql/JavaYouth.git
synced 2026-03-13 21:33:42 +08:00
目录重构
This commit is contained in:
1076
docs/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md
Normal file
1076
docs/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md
Normal file
File diff suppressed because it is too large
Load Diff
1000
docs/JVM/JVM系列-第11章-垃圾回收相关概念.md
Normal file
1000
docs/JVM/JVM系列-第11章-垃圾回收相关概念.md
Normal file
File diff suppressed because it is too large
Load Diff
1195
docs/JVM/JVM系列-第12章-垃圾回收器.md
Normal file
1195
docs/JVM/JVM系列-第12章-垃圾回收器.md
Normal file
File diff suppressed because it is too large
Load Diff
574
docs/JVM/JVM系列-第1章-JVM与Java体系结构.md
Normal file
574
docs/JVM/JVM系列-第1章-JVM与Java体系结构.md
Normal file
@@ -0,0 +1,574 @@
|
||||
---
|
||||
title: JVM系列-第1章-JVM与Java体系结构
|
||||
tags:
|
||||
- JVM
|
||||
- 虚拟机
|
||||
categories:
|
||||
- JVM
|
||||
- 1.内存与垃圾回收篇
|
||||
keywords: JVM,虚拟机。
|
||||
description: JVM系列-第1章-JVM与Java体系结构。
|
||||
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png'
|
||||
abbrlink: 8c954c6
|
||||
date: 2020-11-02 11:51:56
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
> 1、本系列博客,主要是面向Java8的虚拟机。如有特殊说明,会进行标注。
|
||||
>
|
||||
> 2、本系列博客主要参考**尚硅谷的JVM视频教程**,整理不易,所以图片打上了一些水印,还请读者见谅。后续可能会加上一些补充的东西。
|
||||
>
|
||||
> 3、尚硅谷的有些视频还不错(PS:不是广告,毕竟看了人家比较好的教程,得给人家打个call)
|
||||
>
|
||||
> 4、转载请注明出处,多谢~,希望大家一起能维护一个良好的开源环境。
|
||||
|
||||
第1章-JVM和Java体系架构
|
||||
=====================
|
||||
|
||||
前言
|
||||
--------
|
||||
|
||||
你是否也遇到过这些问题?
|
||||
|
||||
1. 运行着的线上系统突然卡死,系统无法访问,甚至直接OOM!
|
||||
2. 想解决线上JVM GC问题,但却无从下手。
|
||||
3. 新项目上线,对各种JVM参数设置一脸茫然,直接默认吧然后就JJ了。
|
||||
4. 每次面试之前都要重新背一遍JVM的一些原理概念性的东西,然而面试官却经常问你在实际项目中如何调优VM参数,如何解决GC、OOM等问题,一脸懵逼。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0001.png">
|
||||
|
||||
大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。
|
||||
|
||||
开发人员如何看待上层框架
|
||||
---------
|
||||
|
||||
|
||||
|
||||
1. 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,这其实是一种本末倒置的“病态”。
|
||||
2. 如果我们把核心类库的API比做数学公式的话,那么Java虚拟机的知识就好比公式的推导过程。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0002.png">
|
||||
|
||||
- 计算机系统体系对我们来说越来越远,在不了解底层实现方式的前提下,通过高级语言很容易编写程序代码。但事实上计算机并不认识高级语言。
|
||||
|
||||
|
||||
|
||||
架构师每天都在思考什么?
|
||||
---------
|
||||
|
||||
1. 应该如何让我的系统更快?
|
||||
2. 如何避免系统出现瓶颈?
|
||||
|
||||
**知乎上有条帖子:应该如何看招聘信息,直通年薪50万+?**
|
||||
|
||||
1. 参与现有系统的性能优化,重构,保证平台性能和稳定性
|
||||
2. 根据业务场景和需求,决定技术方向,做技术选型
|
||||
3. 能够独立架构和设计海量数据下高并发分布式解决方案,满足功能和非功能需求
|
||||
4. 解决各类潜在系统风险,核心功能的架构与代码编写
|
||||
5. 分析系统瓶颈,解决各种疑难杂症,性能调优等
|
||||
|
||||
我们为什么要学习JVM
|
||||
-----------
|
||||
|
||||
1. 面试的需要(BATJ、TMD,PKQ等面试都爱问)
|
||||
2. 中高级程序员必备技能
|
||||
- 项目管理、调优的需要
|
||||
3. 追求极客的精神,
|
||||
- 比如:垃圾回收算法、JIT、底层原理
|
||||
|
||||
|
||||
|
||||
Java VS C++
|
||||
-------------
|
||||
|
||||
1. 垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率,但是,垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高扩展性应用和诊断运行时问题的基础,也是Java工程师进阶的必备能力。
|
||||
2. C++语言需要程序员自己来分配内存和回收内存,对于高手来说可能更加舒服,但是对于普通开发者,如果技术实力不够,很容易造成内存泄漏。而Java全部交给JVM进行内存分配和回收,这也是一种趋势,减少程序员的工作量。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0003.png">
|
||||
|
||||
## 什么人需要学JVM?
|
||||
|
||||
1. 拥有一定开发经验的Java开发人员,希望升职加薪
|
||||
2. 软件设计师,架构师
|
||||
3. 系统调优人员
|
||||
4. 虚拟机爱好者,JVM实践者
|
||||
|
||||
推荐及参考书籍
|
||||
------
|
||||
|
||||
**官方文档**
|
||||
|
||||
**英文文档规范**:https://docs.oracle.com/javase/specs/index.html
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0004.png">
|
||||
|
||||
**中文书籍:**
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0005.png">
|
||||
|
||||
> 周志明老师的这本书**非常推荐看**,不过只推荐看第三版,第三版较第二版更新了很多,个人觉得没必要再看第二版。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0006.png">
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0007.png">
|
||||
|
||||
TIOBE排行榜
|
||||
-----------
|
||||
|
||||
**TIOBE 排行榜**:https://www.tiobe.com/tiobe-index/
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0008.png">
|
||||
|
||||
- 世界上没有最好的编程语言,只有最适用于具体应用场景的编程语言。
|
||||
- 目前网上一直流传Java被python,go撼动Java第一的地位。学习者不需要太担心,Java强大的生态圈,也不是说是朝夕之间可以被撼动的。
|
||||
|
||||
|
||||
|
||||
Java生态圈
|
||||
----------
|
||||
|
||||
Java是目前应用最为广泛的软件开发平台之一。随着Java以及Java社区的不断壮大Java 也早已不再是简简单单的一门计算机语言了,它更是一个平台、一种文化、一个社区。
|
||||
|
||||
1. 作为一个平台,Java虚拟机扮演着举足轻重的作用
|
||||
* Groovy、Scala、JRuby、Kotlin等都是Java平台的一部分
|
||||
2. 作为一种文化,Java几乎成为了“开源”的代名词。
|
||||
* 第三方开源软件和框架。如Tomcat、Struts,MyBatis,Spring等。
|
||||
* 就连JDK和JVM自身也有不少开源的实现,如openJDK、Harmony。
|
||||
3. 作为一个社区,Java拥有全世界最多的技术拥护者和开源社区支持,有数不清的论坛和资料。从桌面应用软件、嵌入式开发到企业级应用、后台服务器、中间件,都可以看到Java的身影。其应用形式之复杂、参与人数之众多也令人咋舌。
|
||||
|
||||
|
||||
|
||||
Java-跨平台的语言
|
||||
------------
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0009.png">
|
||||
|
||||
JVM-跨语言的平台
|
||||
------
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0010.png">
|
||||
|
||||
|
||||
|
||||
1. 随着Java7的正式发布,Java虚拟机的设计者们通过JSR-292规范基本实现在Java虚拟机平台上运行非Java语言编写的程序。
|
||||
2. Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,它只关心“字节码”文件。也就是说Java虚拟机拥有语言无关性,并不会单纯地与Java语言“终身绑定”,只要其他编程语言的编译结果满足并包含Java虚拟机的内部指令集、符号表以及其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行。
|
||||
|
||||
- Java不是最强大的语言,但是JVM是最强大的虚拟机
|
||||
|
||||
1. 我们平时说的java字节码,指的是用java语言编译成的字节码。准确的说任何能在jvm平台上执行的字节码格式都是一样的。所以应该统称为:jvm字节码。
|
||||
|
||||
2. 不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的JVM上运行。
|
||||
|
||||
3. Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式——Class文件格式所关联,Class文件中包含了Java虚拟机指令集(或者称为字节码、Bytecodes)和符号表,还有一些其他辅助信息。
|
||||
|
||||
多语言混合编程
|
||||
----------
|
||||
|
||||
1. Java平台上的多语言混合编程正成为主流,通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。
|
||||
|
||||
2. 试想一下,在一个项目之中,并行处理用Clojure语言编写,展示层使用JRuby/Rails,中间层则是Java,每个应用层都将使用不同的编程语言来完成,而且,接口对每一层的开发者都是透明的,各种语言之间的交互不存在任何困难,就像使用自己语言的原生API一样方便,因为它们最终都运行在一个虚拟机之上。
|
||||
|
||||
3. 对这些运行于Java虚拟机之上、Java之外的语言,来自系统级的、底层的支持正在迅速增强,以JSR-292为核心的一系列项目和功能改进(如DaVinci Machine项目、Nashorn引擎、InvokeDynamic指令、java.lang.invoke包等),推动Java虚拟机从“Java语言的虚拟机”向 “多语言虚拟机”的方向发展。
|
||||
|
||||
|
||||
如何真正搞懂JVM?
|
||||
-----------
|
||||
|
||||
1. Java虚拟机非常复杂,要想真正理解它的工作原理,最好的方式就是自己动手编写一个!
|
||||
2. 自己动手写一个Java虚拟机,难吗?
|
||||
3. 天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0011.png">
|
||||
|
||||
Java发展重大事件
|
||||
------------
|
||||
|
||||
* 1990年,在Sun计算机公司中,由Patrick Naughton、MikeSheridan及James Gosling领导的小组Green Team,开发出的新的程序语言,命名为Oak,后期命名为Java
|
||||
* 1995年,Sun正式发布Java和HotJava产品,Java首次公开亮相。
|
||||
* 1996年1月23日Sun Microsystems发布了JDK 1.0。
|
||||
* 1998年,JDK1.2版本发布。同时,Sun发布了JSP/Servlet、EJB规范,以及将Java分成了J2EE、J2SE和J2ME。这表明了Java开始向企业、桌面应用和移动设备应用3大领域挺进。
|
||||
* 2000年,JDK1.3发布,Java HotSpot Virtual Machine正式发布,成为Java的默认虚拟机。
|
||||
* 2002年,JDK1.4发布,古老的Classic虚拟机退出历史舞台。
|
||||
* 2003年年底,Java平台的scala正式发布,同年Groovy也加入了Java阵营。
|
||||
* 2004年,JDK1.5发布。同时JDK1.5改名为JavaSE5.0。
|
||||
* 2006年,JDK6发布。同年,Java开源并建立了OpenJDK。顺理成章,Hotspot虚拟机也成为了OpenJDK中的默认虚拟机。
|
||||
* 2007年,Java平台迎来了新伙伴Clojure。
|
||||
* 2008年,oracle收购了BEA,得到了JRockit虚拟机。
|
||||
* 2009年,Twitter宣布把后台大部分程序从Ruby迁移到Scala,这是Java平台的又一次大规模应用。
|
||||
* 2010年,Oracle收购了Sun,获得Java商标和最真价值的HotSpot虚拟机。此时,Oracle拥有市场占用率最高的两款虚拟机HotSpot和JRockit,并计划在未来对它们进行整合:HotRockit。JCP组织管理Java语言
|
||||
* 2011年,JDK7发布。在JDK1.7u4中,正式启用了新的垃圾回收器G1。
|
||||
* **2017年,JDK9发布。将G1设置为默认GC,替代CMS**
|
||||
* 同年,IBM的J9开源,形成了现在的Open J9社区
|
||||
* 2018年,Android的Java侵权案判决,Google赔偿Oracle计88亿美元
|
||||
* 同年,Oracle宣告JavagE成为历史名词JDBC、JMS、Servlet赠予Eclipse基金会
|
||||
* **同年,JDK11发布,LTS版本的JDK,发布革命性的ZGC,调整JDK授权许可**
|
||||
* 2019年,JDK12发布,加入RedHat领导开发的Shenandoah GC
|
||||
|
||||
## Open JDK和Oracle JDK
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0012.png">
|
||||
|
||||
- 在JDK11之前,Oracle JDK中还会存在一些Open JDK中没有的,闭源的功能。但在JDK11中,我们可以认为Open JDK和Oracle JDK代码实质上已经达到完全一致的程度了。
|
||||
- 主要的区别就是两者更新周期不一样
|
||||
|
||||
虚拟机
|
||||
--------
|
||||
|
||||
### 虚拟机概念
|
||||
|
||||
- 所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
|
||||
|
||||
- 大名鼎鼎的Virtual Box,VMware就属于系统虚拟机,它们完全是对物理计算机硬件的仿真(模拟),提供了一个可运行完整操作系统的软件平台。
|
||||
|
||||
+ 程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。
|
||||
|
||||
- 无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。
|
||||
|
||||
### Java虚拟机
|
||||
|
||||
1. Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
|
||||
2. JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
|
||||
3. **Java技术的核心就是Java虚拟机**(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
|
||||
|
||||
|
||||
|
||||
**作用:**
|
||||
|
||||
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
|
||||
|
||||
**特点:**
|
||||
|
||||
1. 一次编译,到处运行
|
||||
2. 自动内存管理
|
||||
3. 自动垃圾回收功能
|
||||
|
||||
|
||||
|
||||
JVM的位置
|
||||
----------
|
||||
|
||||
JVM是运行在操作系统之上的,它与硬件没有直接的交互
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0013.png">
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0014.png">
|
||||
|
||||
|
||||
|
||||
JVM的整体结构
|
||||
-------------
|
||||
|
||||
1. HotSpot VM是目前市面上高性能虚拟机的代表作之一。
|
||||
2. 它采用解释器与即时编译器并存的架构。
|
||||
3. 在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0015.png">
|
||||
|
||||
|
||||
|
||||
Java代码执行流程
|
||||
--------------
|
||||
|
||||
凡是能生成被Java虚拟机所能解释、运行的字节码文件,那么理论上我们就可以自己设计一套语言了
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_001/0016.png">
|
||||
|
||||
|
||||
|
||||
JVM的架构模型
|
||||
-----------
|
||||
|
||||
Java编译器输入的指令流基本上是一种**基于栈的指令集架构**,另外一种指令集架构则是**基于寄存器的指令集架构**。具体来说:这两种架构之间的区别:
|
||||
|
||||
### 基于栈的指令集架构
|
||||
|
||||
基于栈式架构的特点:
|
||||
|
||||
1. 设计和实现更简单,适用于资源受限的系统;
|
||||
2. 避开了寄存器的分配难题:使用零地址指令方式分配
|
||||
3. 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现
|
||||
4. 不需要硬件支持,可移植性更好,更好实现跨平台
|
||||
|
||||
### 基于寄存器的指令级架构
|
||||
|
||||
基于寄存器架构的特点:
|
||||
|
||||
1. 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
|
||||
2. 指令集架构则完全依赖硬件,与硬件的耦合度高,可移植性差
|
||||
3. 性能优秀和执行更高效
|
||||
4. 花费更少的指令去完成一项操作
|
||||
5. 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主
|
||||
|
||||
### 两种架构的举例
|
||||
|
||||
同样执行2+3这种逻辑操作,其指令分别如下:
|
||||
|
||||
* **基于栈的计算流程(以Java虚拟机为例):**
|
||||
|
||||
```java
|
||||
iconst_2 //常量2入栈
|
||||
istore_1
|
||||
iconst_3 // 常量3入栈
|
||||
istore_2
|
||||
iload_1
|
||||
iload_2
|
||||
iadd //常量2/3出栈,执行相加
|
||||
istore_0 // 结果5入栈
|
||||
```
|
||||
|
||||
8个指令
|
||||
|
||||
|
||||
|
||||
* **而基于寄存器的计算流程**
|
||||
|
||||
```java
|
||||
mov eax,2 //将eax寄存器的值设为1
|
||||
add eax,3 //使eax寄存器的值加3
|
||||
```
|
||||
|
||||
2个指令
|
||||
|
||||
> 具体后面会讲
|
||||
|
||||
|
||||
|
||||
### JVM架构总结
|
||||
|
||||
1. **由于跨平台性的设计,Java的指令都是根据栈来设计的**。不同平台CPU架构不同,所以不能设计为基于寄存器的。栈的优点:跨平台,指令集小,编译器容易实现,缺点是性能比寄存器差一些。
|
||||
|
||||
2. 时至今日,尽管嵌入式平台已经不是Java程序的主流运行平台了(准确来说应该是HotSpot VM的宿主环境已经不局限于嵌入式平台了),那么为什么不将架构更换为基于寄存器的架构呢?
|
||||
- 因为基于栈的架构跨平台性好、指令集小,虽然相对于基于寄存器的架构来说,基于栈的架构编译得到的指令更多,执行性能也不如基于寄存器的架构好,但考虑到其跨平台性与移植性,我们还是选用栈的架构
|
||||
|
||||
|
||||
|
||||
|
||||
JVM的生命周期
|
||||
-----------
|
||||
|
||||
### 虚拟机的启动
|
||||
|
||||
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
|
||||
|
||||
### 虚拟机的执行
|
||||
|
||||
1. 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
|
||||
2. 程序开始执行时他才运行,程序结束时他就停止
|
||||
3. **执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程**
|
||||
|
||||
### 虚拟机的退出
|
||||
|
||||
**有如下的几种情况:**
|
||||
|
||||
1. 程序正常执行结束
|
||||
|
||||
2. 程序在执行过程中遇到了异常或错误而异常终止
|
||||
|
||||
3. 由于操作系统用现错误而导致Java虚拟机进程终止
|
||||
|
||||
4. 某线程调用Runtime类或System类的exit()方法,或Runtime类的halt()方法,并且Java安全管理器也允许这次exit()或halt()操作。
|
||||
|
||||
5. 除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载 Java虚拟机时,Java虚拟机的退出情况。
|
||||
|
||||
|
||||
|
||||
|
||||
JVM发展历程
|
||||
-----------
|
||||
|
||||
### Sun Classic VM
|
||||
|
||||
1. 早在1996年Java1.0版本的时候,Sun公司发布了一款名为sun classic VM的Java虚拟机,它同时也是**世界上第一款商用Java虚拟机**,JDK1.4时完全被淘汰。
|
||||
2. 这款虚拟机内部只提供解释器,没有即时编译器,因此效率比较低。【即时编译器会把热点代码的本地机器指令缓存起来,那么以后使用热点代码的时候,效率就比较高】
|
||||
3. 如果使用JIT编译器,就需要进行外挂。但是一旦使用了JIT编译器,JIT就会接管虚拟机的执行系统。解释器就不再工作,解释器和编译器不能配合工作。
|
||||
- 我们将字节码指令翻译成机器指令也是需要花时间的,如果只使用JIT,就需要把所有字节码指令都翻译成机器指令,就会导致翻译时间过长,也就是说在程序刚启动的时候,等待时间会很长。
|
||||
- 而解释器就是走到哪,解释到哪。
|
||||
4. 现在Hotspot内置了此虚拟机。
|
||||
|
||||
### Exact VM
|
||||
|
||||
1. 为了解决上一个虚拟机问题,jdk1.2时,Sun提供了此虚拟机。
|
||||
|
||||
2. Exact Memory Management:准确式内存管理
|
||||
|
||||
* 也可以叫Non-Conservative/Accurate Memory Management
|
||||
|
||||
* 虚拟机可以知道内存中某个位置的数据具体是什么类型。
|
||||
|
||||
3. 具备现代高性能虚拟机的维形
|
||||
|
||||
* 热点探测(寻找出热点代码进行缓存)
|
||||
|
||||
* 编译器与解释器混合工作模式
|
||||
|
||||
4. 只在Solaris平台短暂使用,其他平台上还是classic vm,英雄气短,终被Hotspot虚拟机替换
|
||||
|
||||
|
||||
### HotSpot VM(重点)
|
||||
|
||||
1. HotSpot历史
|
||||
|
||||
* 最初由一家名为“Longview Technologies”的小公司设计
|
||||
|
||||
* 1997年,此公司被Sun收购;2009年,Sun公司被甲骨文收购。
|
||||
|
||||
* JDK1.3时,HotSpot VM成为默认虚拟机
|
||||
|
||||
2. 目前**Hotspot占有绝对的市场地位,称霸武林**。
|
||||
|
||||
* 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是HotSpot
|
||||
|
||||
* Sun/oracle JDK和openJDK的默认虚拟机
|
||||
|
||||
* 因此本课程中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。(比如其他两个商用虚机都没有方法区的概念)
|
||||
|
||||
3. 从服务器、桌面到移动端、嵌入式都有应用。
|
||||
|
||||
4. 名称中的HotSpot指的就是它的热点代码探测技术。
|
||||
|
||||
* 通过计数器找到最具编译价值代码,触发即时编译或栈上替换
|
||||
|
||||
* 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡
|
||||
|
||||
### JRockit(商用三大虚拟机之一)
|
||||
|
||||
1. 专注于服务器端应用:它可以不太关注程序启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行。
|
||||
|
||||
2. 大量的行业基准测试显示,JRockit JVM是世界上最快的JVM:使用JRockit产品,客户已经体验到了显著的性能提高(一些超过了70%)和硬件成本的减少(达50%)。
|
||||
|
||||
3. 优势:全面的Java运行时解决方案组合
|
||||
|
||||
* JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒级的JVM响应时间,适合财务、军事指挥、电信网络的需要
|
||||
|
||||
* Mission Control服务套件,它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。
|
||||
|
||||
4. 2008年,JRockit被Oracle收购。
|
||||
|
||||
5. Oracle表达了整合两大优秀虚拟机的工作,大致在JDK8中完成。整合的方式是在HotSpot的基础上,移植JRockit的优秀特性。
|
||||
|
||||
6. 高斯林:目前就职于谷歌,研究人工智能和水下机器人
|
||||
|
||||
|
||||
### IBM的J9(商用三大虚拟机之一)
|
||||
|
||||
1. 全称:IBM Technology for Java Virtual Machine,简称IT4J,内部代号:J9
|
||||
|
||||
2. 市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM广泛用于IBM的各种Java产品。
|
||||
|
||||
3. 目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机。
|
||||
|
||||
4. 2017年左右,IBM发布了开源J9VM,命名为openJ9,交给Eclipse基金会管理,也称为Eclipse OpenJ9
|
||||
|
||||
5. OpenJDK -> 是JDK开源了,包括了虚拟机
|
||||
|
||||
|
||||
### KVM和CDC/CLDC Hotspot
|
||||
|
||||
1. Oracle在Java ME产品线上的两款虚拟机为:CDC/CLDC HotSpot Implementation VM
|
||||
2. KVM(Kilobyte)是CLDC-HI早期产品
|
||||
3. 目前移动领域地位尴尬,智能机被Android和iOS二分天下。
|
||||
4. KVM简单、轻量、高度可移植,面向更低端的设备上还维持自己的一片市场
|
||||
|
||||
* 智能控制器、传感器
|
||||
|
||||
* 老人手机、经济欠发达地区的功能手机
|
||||
5. 所有的虚拟机的原则:一次编译,到处运行。
|
||||
|
||||
|
||||
### Azul VM
|
||||
|
||||
1. 前面三大“高性能Java虚拟机”使用在**通用硬件平台上**
|
||||
2. 这里Azul VW和BEA Liquid VM是与**特定硬件平台绑定**、软硬件配合的专有虚拟机:高性能Java虚拟机中的战斗机。
|
||||
3. Azul VM是Azul Systems公司在HotSpot基础上进行大量改进,运行于Azul Systems公司的专有硬件Vega系统上的Java虚拟机。
|
||||
4. 每个Azul VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器、专有硬件优化的线程调度等优秀特性。
|
||||
5. 2010年,Azul Systems公司开始从硬件转向软件,发布了自己的Zing JVM,可以在通用x86平台上提供接近于Vega系统的特性。
|
||||
|
||||
|
||||
### Liquid VM
|
||||
|
||||
1. 高性能Java虚拟机中的战斗机。
|
||||
2. BEA公司开发的,直接运行在自家Hypervisor系统上
|
||||
3. Liquid VM即是现在的JRockit VE(Virtual Edition)。**Liquid VM不需要操作系统的支持,或者说它自己本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等**。
|
||||
5. 随着JRockit虚拟机终止开发,Liquid vM项目也停止了。
|
||||
|
||||
### Apache Marmony
|
||||
|
||||
1. Apache也曾经推出过与JDK1.5和JDK1.6兼容的Java运行平台Apache Harmony。
|
||||
|
||||
2. 它是IElf和Intel联合开发的开源JVM,受到同样开源的Open JDK的压制,Sun坚决不让Harmony获得JCP认证,最终于2011年退役,IBM转而参与OpenJDK
|
||||
|
||||
3. 虽然目前并没有Apache Harmony被大规模商用的案例,但是它的Java类库代码吸纳进了Android SDK。
|
||||
|
||||
|
||||
### Micorsoft JVM
|
||||
|
||||
1. 微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM。
|
||||
|
||||
2. 只能在window平台下运行。但确是当时Windows下性能最好的Java VM。
|
||||
|
||||
3. 1997年,Sun以侵犯商标、不正当竞争罪名指控微软成功,赔了Sun很多钱。微软WindowsXP SP3中抹掉了其VM。现在Windows上安装的jdk都是HotSpot。
|
||||
|
||||
|
||||
### Taobao JVM
|
||||
|
||||
1. 由AliJVM团队发布。阿里,国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域,需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。
|
||||
|
||||
2. **基于OpenJDK开发了自己的定制版本AlibabaJDK**,简称AJDK。是整个阿里Java体系的基石。
|
||||
|
||||
3. 基于OpenJDK Hotspot VM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机。
|
||||
|
||||
* 创新的GCIH(GCinvisible heap)技术实现了off-heap,**即将生命周期较长的Java对象从heap中移到heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的**。
|
||||
* GCIH中的**对象还能够在多个Java虚拟机进程中实现共享**
|
||||
* 使用crc32指令实现JvM intrinsic降低JNI的调用开销
|
||||
* PMU hardware的Java profiling tool和诊断协助功能
|
||||
* 针对大数据场景的ZenGC
|
||||
4. taobao vm应用在阿里产品上性能高,**硬件严重依赖inte1的cpu,损失了兼容性,但提高了性能**
|
||||
- 目前已经在淘宝、天猫上线,把Oracle官方JvM版本全部替换了。
|
||||
|
||||
|
||||
### Dalvik VM
|
||||
|
||||
1. 谷歌开发的,应用于Android系统,并在Android2.2中提供了JIT,发展迅猛。
|
||||
|
||||
2. **Dalvik VM只能称作虚拟机,而不能称作“Java虚拟机”**,它没有遵循 Java虚拟机规范
|
||||
|
||||
3. 不能直接执行Java的Class文件
|
||||
|
||||
4. 基于寄存器架构,不是jvm的栈架构。
|
||||
|
||||
5. 执行的是编译以后的dex(Dalvik Executable)文件。执行效率比较高。
|
||||
- 它执行的dex(Dalvik Executable)文件可以通过class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。
|
||||
|
||||
7. Android 5.0使用支持提前编译(Ahead of Time Compilation,AoT)的ART VM替换Dalvik VM。
|
||||
|
||||
|
||||
### Graal VM(未来虚拟机)
|
||||
|
||||
1. 2018年4月,Oracle Labs公开了GraalvM,号称 “**Run Programs Faster Anywhere**”,勃勃野心。与1995年java的”write once,run anywhere"遥相呼应。
|
||||
|
||||
2. GraalVM在HotSpot VM基础上增强而成的**跨语言全栈虚拟机,可以作为“任何语言”**的运行平台使用。语言包括:Java、Scala、Groovy、Kotlin;C、C++、Javascript、Ruby、Python、R等
|
||||
|
||||
3. 支持不同语言中混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件
|
||||
|
||||
4. 工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中间表示。Graal VM提供Truffle工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化,获得比原生编译器更优秀的执行效率。
|
||||
|
||||
5. **如果说HotSpot有一天真的被取代,Graalvm希望最大**。但是Java的软件生态没有丝毫变化。
|
||||
|
||||
|
||||
|
||||
### 总结
|
||||
|
||||
具体JVM的内存结构,其实取决于其实现,不同厂商的JVM,或者同一厂商发布的不同版本,都有可能存在一定差异。主要以Oracle HotSpot VM为默认虚拟机。
|
||||
817
docs/JVM/JVM系列-第2章-类加载子系统.md
Normal file
817
docs/JVM/JVM系列-第2章-类加载子系统.md
Normal file
@@ -0,0 +1,817 @@
|
||||
---
|
||||
title: JVM系列-第2章-类加载子系统
|
||||
tags:
|
||||
- JVM
|
||||
- 虚拟机
|
||||
categories:
|
||||
- JVM
|
||||
- 1.内存与垃圾回收篇
|
||||
keywords: JVM,虚拟机。
|
||||
description: JVM系列-第2章-类加载子系统。
|
||||
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png'
|
||||
abbrlink: 2e0079af
|
||||
date: 2020-11-02 21:31:58
|
||||
---
|
||||
|
||||
|
||||
|
||||
第2章-类加载子系统
|
||||
============
|
||||
|
||||
内存结构概述
|
||||
--------
|
||||
|
||||
|
||||
|
||||
### 简图
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0001.png">
|
||||
|
||||
### 详细图
|
||||
|
||||
英文版
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0002.jpg">
|
||||
|
||||
|
||||
|
||||
中文版
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0003.jpg">
|
||||
|
||||
注意:方法区只有HotSpot虚拟机有,J9,JRockit都没有
|
||||
|
||||
|
||||
|
||||
如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
|
||||
|
||||
1. 类加载器
|
||||
2. 执行引擎
|
||||
|
||||
类加载器子系统
|
||||
--------
|
||||
|
||||
**类加载器子系统作用:**
|
||||
|
||||
1. 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
|
||||
|
||||
2. ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
|
||||
|
||||
3. **加载的类信息存放于一块称为方法区的内存空间**。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0004.png">
|
||||
|
||||
|
||||
|
||||
## 类加载器ClassLoader角色
|
||||
|
||||
1. class file(在下图中就是Car.class文件)存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
|
||||
2. class file加载到JVM中,被称为DNA元数据模板(在下图中就是内存中的Car Class),放在方法区。
|
||||
3. 在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0005.png">
|
||||
|
||||
类加载过程
|
||||
-------
|
||||
|
||||
### 概述
|
||||
|
||||
```java
|
||||
public class HelloLoader {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("谢谢ClassLoader加载我....");
|
||||
System.out.println("你的大恩大德,我下辈子再报!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
它的加载过程是怎么样的呢?
|
||||
|
||||
* 执行 main() 方法(静态方法)就需要先加载main方法所在类 HelloLoader
|
||||
* 加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main
|
||||
* 加载失败则抛出异常
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0006.png">
|
||||
|
||||
完整的流程图如下所示:
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0007.png">
|
||||
|
||||
|
||||
|
||||
### 加载阶段
|
||||
|
||||
**加载:**
|
||||
|
||||
1. 通过一个类的全限定名获取定义此类的二进制字节流
|
||||
|
||||
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
|
||||
|
||||
3. **在内存中生成一个代表这个类的java.lang.Class对象**,作为方法区这个类的各种数据的访问入口
|
||||
|
||||
**加载class文件的方式:**
|
||||
|
||||
1. 从本地系统中直接加载
|
||||
2. 通过网络获取,典型场景:Web Applet
|
||||
3. 从zip压缩包中读取,成为日后jar、war格式的基础
|
||||
4. 运行时计算生成,使用最多的是:动态代理技术
|
||||
5. 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
|
||||
6. 从加密文件中获取,典型的防Class文件被反编译的保护措施
|
||||
|
||||
|
||||
|
||||
### 链接阶段
|
||||
|
||||
链接分为三个子阶段:验证 -> 准备 -> 解析
|
||||
|
||||
|
||||
|
||||
#### 验证(Verify)
|
||||
|
||||
1. 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
|
||||
2. 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
|
||||
|
||||
|
||||
|
||||
**举例**
|
||||
|
||||
使用 BinaryViewer软件查看字节码文件,其开头均为 CAFE BABE ,如果出现不合法的字节码文件,那么将会验证不通过。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0008.png">
|
||||
|
||||
#### 准备(Prepare)
|
||||
|
||||
|
||||
|
||||
1. 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
|
||||
2. 这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显式初始化
|
||||
3. 注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
|
||||
|
||||
**举例**
|
||||
|
||||
代码:变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1
|
||||
|
||||
```java
|
||||
public class HelloApp {
|
||||
private static int a = 1;//prepare:a = 0 ---> initial : a = 1
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(a);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 解析(Resolve)
|
||||
|
||||
|
||||
|
||||
1. **将常量池内的符号引用转换为直接引用的过程**
|
||||
|
||||
2. 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
|
||||
|
||||
3. 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
|
||||
|
||||
4. 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
|
||||
|
||||
**符号引用**
|
||||
|
||||
* 反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0023.png"/>
|
||||
|
||||
### 初始化阶段
|
||||
|
||||
#### 类的初始化时机
|
||||
|
||||
1. 创建类的实例
|
||||
2. 访问某个类或接口的静态变量,或者对该静态变量赋值
|
||||
3. 调用类的静态方法
|
||||
4. 反射(比如:Class.forName(“com.atguigu.Test”))
|
||||
5. 初始化一个类的子类
|
||||
6. Java虚拟机启动时被标明为启动类的类
|
||||
7. JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
|
||||
|
||||
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段(不会调用 clinit() 方法和 init() 方法)
|
||||
|
||||
|
||||
|
||||
### clinit()
|
||||
|
||||
1. 初始化阶段就是执行类构造器方法`<clinit>()`的过程
|
||||
|
||||
2. 此方法不需定义,是javac编译器自动收集类中的所有**类变量**的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法
|
||||
|
||||
3. `<clinit>()`方法中的指令按语句在源文件中出现的顺序执行
|
||||
|
||||
4. `<clinit>()`不同于类的构造器。(关联:构造器是虚拟机视角下的`<init>()`)
|
||||
|
||||
5. 若该类具有父类,JVM会保证子类的`<clinit>()`执行前,父类的`<clinit>()`已经执行完毕
|
||||
|
||||
6. 虚拟机必须保证一个类的`<clinit>()`方法在多线程下被同步加锁
|
||||
|
||||
> IDEA 中安装 JClassLib Bytecode viewer 插件,可以很方便的看字节码。安装过程可以自行百度
|
||||
|
||||
#### 1,2,3说明
|
||||
|
||||
**举例1:有static变量**
|
||||
|
||||
查看下面这个代码的字节码,可以发现有一个`<clinit>()`方法。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0009.png">
|
||||
|
||||
```java
|
||||
public class ClassInitTest {
|
||||
private static int num = 1;
|
||||
|
||||
static{
|
||||
num = 2;
|
||||
number = 20;
|
||||
System.out.println(num);
|
||||
//System.out.println(number);//报错:非法的前向引用。
|
||||
}
|
||||
|
||||
/**
|
||||
* 1、linking之prepare: number = 0 --> initial: 20 --> 10
|
||||
* 2、这里因为静态代码块出现在声明变量语句前面,所以之前被准备阶段为0的number变量会
|
||||
* 首先被初始化为20,再接着被初始化成10(这也是面试时常考的问题哦)
|
||||
*
|
||||
*/
|
||||
private static int number = 10;
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(ClassInitTest.num);//2
|
||||
System.out.println(ClassInitTest.number);//10
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<clint字节码>:
|
||||
|
||||
```java
|
||||
0 iconst_1
|
||||
1 putstatic #3 <com/atguigu/java/ClassInitTest.num>
|
||||
4 iconst_2
|
||||
5 putstatic #3 <com/atguigu/java/ClassInitTest.num>
|
||||
8 bipush 20 //先赋20
|
||||
10 putstatic #5 <com/atguigu/java/ClassInitTest.number>
|
||||
13 getstatic #2 <java/lang/System.out>
|
||||
16 getstatic #3 <com/atguigu/java/ClassInitTest.num>
|
||||
19 invokevirtual #4 <java/io/PrintStream.println>
|
||||
22 bipush 10 //再赋10
|
||||
24 putstatic #5 <com/atguigu/java/ClassInitTest.number>
|
||||
27 return
|
||||
```
|
||||
|
||||
|
||||
|
||||
当我们代码中包含static变量的时候,就会有clinit方法
|
||||
|
||||
**举例2:无 static 变量**
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0010.png">
|
||||
|
||||
加上之后就有了
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0011.png">
|
||||
|
||||
#### 4说明
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0012.png">
|
||||
|
||||
在构造器中:
|
||||
|
||||
* 先将类变量 a 赋值为 10
|
||||
* 再将局部变量赋值为 20
|
||||
|
||||
#### 5说明
|
||||
|
||||
若该类具有父类,JVM会保证子类的`<clinit>()`执行前,父类的`<clinit>()`已经执行完毕
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0013.png">
|
||||
|
||||
如上代码,加载流程如下:
|
||||
|
||||
* 首先,执行 main() 方法需要加载 ClinitTest1 类
|
||||
* 获取 Son.B 静态变量,需要加载 Son 类
|
||||
* Son 类的父类是 Father 类,所以需要先执行 Father 类的加载,再执行 Son 类的加载
|
||||
|
||||
|
||||
|
||||
#### 6说明
|
||||
|
||||
虚拟机必须保证一个类的`<clinit>()`方法在多线程下被同步加锁
|
||||
|
||||
```java
|
||||
public class DeadThreadTest {
|
||||
public static void main(String[] args) {
|
||||
Runnable r = () -> {
|
||||
System.out.println(Thread.currentThread().getName() + "开始");
|
||||
DeadThread dead = new DeadThread();
|
||||
System.out.println(Thread.currentThread().getName() + "结束");
|
||||
};
|
||||
|
||||
Thread t1 = new Thread(r,"线程1");
|
||||
Thread t2 = new Thread(r,"线程2");
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
}
|
||||
}
|
||||
|
||||
class DeadThread{
|
||||
static{
|
||||
if(true){
|
||||
System.out.println(Thread.currentThread().getName() + "初始化当前类");
|
||||
while(true){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
线程2开始
|
||||
线程1开始
|
||||
线程2初始化当前类
|
||||
|
||||
/然后程序卡死了
|
||||
```
|
||||
|
||||
|
||||
|
||||
程序卡死,分析原因:
|
||||
|
||||
* 两个线程同时去加载 DeadThread 类,而 DeadThread 类中静态代码块中有一处死循环
|
||||
* 先加载 DeadThread 类的线程抢到了同步锁,然后在类的静态代码块中执行死循环,而另一个线程在等待同步锁的释放
|
||||
* 所以无论哪个线程先执行 DeadThread 类的加载,另外一个类也不会继续执行。(一个类只会被加载一次)
|
||||
|
||||
|
||||
|
||||
类加载器的分类
|
||||
---------
|
||||
|
||||
### 概述
|
||||
|
||||
|
||||
|
||||
1. JVM严格来讲支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
|
||||
|
||||
2. 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是**将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器**
|
||||
|
||||
3. 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0014.png">
|
||||
|
||||
|
||||
|
||||
**ExtClassLoader**
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0015.png">
|
||||
|
||||
**AppClassLoader**
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0016.png">
|
||||
|
||||
|
||||
|
||||
```java
|
||||
public class ClassLoaderTest {
|
||||
public static void main(String[] args) {
|
||||
|
||||
//获取系统类加载器
|
||||
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
|
||||
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
|
||||
|
||||
//获取其上层:扩展类加载器
|
||||
ClassLoader extClassLoader = systemClassLoader.getParent();
|
||||
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
|
||||
|
||||
//获取其上层:获取不到引导类加载器
|
||||
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
|
||||
System.out.println(bootstrapClassLoader);//null
|
||||
|
||||
//对于用户自定义类来说:默认使用系统类加载器进行加载
|
||||
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
|
||||
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
|
||||
|
||||
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
|
||||
ClassLoader classLoader1 = String.class.getClassLoader();
|
||||
System.out.println(classLoader1);//null
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
* 我们尝试获取引导类加载器,获取到的值为 null ,这并不代表引导类加载器不存在,**因为引导类加载器右 C/C++ 语言,我们获取不到**
|
||||
* 两次获取系统类加载器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,这说明**系统类加载器是全局唯一的**
|
||||
|
||||
|
||||
|
||||
### 虚拟机自带的加载器
|
||||
|
||||
#### 启动类加载器
|
||||
|
||||
> **启动类加载器(引导类加载器,Bootstrap ClassLoader)**
|
||||
|
||||
1. 这个类加载使用C/C++语言实现的,嵌套在JVM内部
|
||||
2. 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
|
||||
3. 并不继承自java.lang.ClassLoader,没有父加载器
|
||||
4. 加载扩展类和应用程序类加载器,并作为他们的父类加载器
|
||||
5. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
|
||||
|
||||
|
||||
|
||||
#### 扩展类加载器
|
||||
|
||||
> **扩展类加载器(Extension ClassLoader)**
|
||||
|
||||
1. Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
|
||||
2. 派生于ClassLoader类
|
||||
3. 父类加载器为启动类加载器
|
||||
4. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
|
||||
|
||||
|
||||
|
||||
#### 系统类加载器
|
||||
|
||||
> **应用程序类加载器(也称为系统类加载器,AppClassLoader)**
|
||||
|
||||
1. Java语言编写,由sun.misc.LaunchersAppClassLoader实现
|
||||
2. 派生于ClassLoader类
|
||||
3. 父类加载器为扩展类加载器
|
||||
4. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
|
||||
5. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
|
||||
6. 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器
|
||||
|
||||
|
||||
|
||||
```java
|
||||
public class ClassLoaderTest1 {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("**********启动类加载器**************");
|
||||
//获取BootstrapClassLoader能够加载的api的路径
|
||||
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
|
||||
for (URL element : urLs) {
|
||||
System.out.println(element.toExternalForm());
|
||||
}
|
||||
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
|
||||
ClassLoader classLoader = Provider.class.getClassLoader();
|
||||
System.out.println(classLoader);
|
||||
|
||||
System.out.println("***********扩展类加载器*************");
|
||||
String extDirs = System.getProperty("java.ext.dirs");
|
||||
for (String path : extDirs.split(";")) {
|
||||
System.out.println(path);
|
||||
}
|
||||
|
||||
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
|
||||
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
|
||||
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
**输出结果**
|
||||
|
||||
```java
|
||||
**********启动类加载器**************
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/resources.jar
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/rt.jar
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jsse.jar
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jce.jar
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/charsets.jar
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jfr.jar
|
||||
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/classes
|
||||
null
|
||||
***********扩展类加载器*************
|
||||
C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext
|
||||
C:\Windows\Sun\Java\lib\ext
|
||||
sun.misc.Launcher$ExtClassLoader@29453f44
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 用户自定义类加载器
|
||||
|
||||
#### 什么时候需要自定义类加载器?
|
||||
|
||||
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。那为什么还需要自定义类加载器?
|
||||
|
||||
1. 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)
|
||||
2. 修改类加载的方式
|
||||
3. 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)
|
||||
4. 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)
|
||||
|
||||
#### 如何自定义类加载器?
|
||||
|
||||
1. 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
|
||||
2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
|
||||
3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
|
||||
|
||||
**代码示例**
|
||||
|
||||
```java
|
||||
public class CustomClassLoader extends ClassLoader {
|
||||
@Override
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
|
||||
try {
|
||||
byte[] result = getClassFromCustomPath(name);
|
||||
if (result == null) {
|
||||
throw new FileNotFoundException();
|
||||
} else {
|
||||
//defineClass和findClass搭配使用
|
||||
return defineClass(name, result, 0, result.length);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
//自定义流的获取方式
|
||||
private byte[] getClassFromCustomPath(String name) {
|
||||
//从自定义路径中加载指定类:细节略
|
||||
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
CustomClassLoader customClassLoader = new CustomClassLoader();
|
||||
try {
|
||||
Class<?> clazz = Class.forName("One", true, customClassLoader);
|
||||
Object obj = clazz.newInstance();
|
||||
System.out.println(obj.getClass().getClassLoader());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 关于ClassLoader
|
||||
|
||||
> **ClassLoader 类介绍**
|
||||
|
||||
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0017.png">
|
||||
|
||||
sun.misc.Launcher 它是一个java虚拟机的入口应用
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0018.png">
|
||||
|
||||
#### 获取ClassLoader途径
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0019.png">
|
||||
|
||||
|
||||
|
||||
```java
|
||||
public class ClassLoaderTest2 {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
//1.
|
||||
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
|
||||
System.out.println(classLoader);
|
||||
//2.
|
||||
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
|
||||
System.out.println(classLoader1);
|
||||
|
||||
//3.
|
||||
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
|
||||
System.out.println(classLoader2);
|
||||
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
null
|
||||
sun.misc.Launcher$AppClassLoader@18b4aac2
|
||||
sun.misc.Launcher$ExtClassLoader@1540e19d
|
||||
|
||||
Process finished with exit code 0
|
||||
```
|
||||
|
||||
|
||||
|
||||
双亲委派机制
|
||||
--------
|
||||
|
||||
### 双亲委派机制原理
|
||||
|
||||
|
||||
|
||||
Java虚拟机对class文件采用的是**按需加载**的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
|
||||
|
||||
1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
|
||||
2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
|
||||
3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
|
||||
4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0020.png">
|
||||
|
||||
### 双亲委派机制代码演示
|
||||
|
||||
|
||||
|
||||
#### 举例1
|
||||
|
||||
1、我们自己建立一个 java.lang.String 类,写上 static 代码块
|
||||
|
||||
```java
|
||||
public class String {
|
||||
//
|
||||
static{
|
||||
System.out.println("我是自定义的String类的静态代码块");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
2、在另外的程序中加载 String 类,看看加载的 String 类是 JDK 自带的 String 类,还是我们自己编写的 String 类
|
||||
|
||||
```java
|
||||
public class StringTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
java.lang.String str = new java.lang.String();
|
||||
System.out.println("hello,atguigu.com");
|
||||
|
||||
StringTest test = new StringTest();
|
||||
System.out.println(test.getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
hello,atguigu.com
|
||||
sun.misc.Launcher$AppClassLoader@18b4aac2
|
||||
```
|
||||
|
||||
|
||||
|
||||
程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
把刚刚的类改一下
|
||||
|
||||
```java
|
||||
package java.lang;
|
||||
public class String {
|
||||
//
|
||||
static{
|
||||
System.out.println("我是自定义的String类的静态代码块");
|
||||
}
|
||||
//错误: 在类 java.lang.String 中找不到 main 方法
|
||||
public static void main(String[] args) {
|
||||
System.out.println("hello,String");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0021.png">
|
||||
|
||||
由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误。
|
||||
|
||||
|
||||
|
||||
#### 举例2
|
||||
|
||||
```java
|
||||
package java.lang;
|
||||
|
||||
|
||||
public class ShkStart {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("hello!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```java
|
||||
java.lang.SecurityException: Prohibited package name: java.lang
|
||||
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
|
||||
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
|
||||
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
|
||||
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
|
||||
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
|
||||
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
|
||||
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
|
||||
at java.security.AccessController.doPrivileged(Native Method)
|
||||
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
|
||||
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
|
||||
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
|
||||
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
|
||||
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
|
||||
Error: A JNI error has occurred, please check your installation and try again
|
||||
Exception in thread "main"
|
||||
Process finished with exit code 1
|
||||
```
|
||||
|
||||
即使类名没有重复,也禁止使用java.lang这种包名。这是一种保护机制
|
||||
|
||||
|
||||
|
||||
#### 举例3
|
||||
|
||||
当我们加载jdbc.jar 用于实现数据库连接的时候
|
||||
|
||||
1. 我们现在程序中需要用到SPI接口,而SPI接口属于rt.jar包中Java核心api
|
||||
2. 然后使用双清委派机制,引导类加载器把rt.jar包加载进来,而rt.jar包中的SPI存在一些接口,接口我们就需要具体的实现类了
|
||||
3. 具体的实现类就涉及到了某些第三方的jar包了,比如我们加载SPI的实现类jdbc.jar包【首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的】
|
||||
4. 第三方的jar包中的类属于系统类加载器来加载
|
||||
5. 从这里面就可以看到SPI核心接口由引导类加载器来加载,SPI具体实现类由系统类加载器来加载
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_002/0022.png">
|
||||
|
||||
|
||||
|
||||
### 双亲委派机制优势
|
||||
|
||||
|
||||
|
||||
通过上面的例子,我们可以知道,双亲机制可以
|
||||
|
||||
1. 避免类的重复加载
|
||||
|
||||
2. 保护程序安全,防止核心API被随意篡改
|
||||
|
||||
- 自定义类:自定义java.lang.String 没有被加载。
|
||||
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
|
||||
|
||||
|
||||
|
||||
沙箱安全机制
|
||||
--------
|
||||
|
||||
|
||||
|
||||
1. 自定义String类时:在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。
|
||||
2. 这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
|
||||
|
||||
|
||||
|
||||
其他
|
||||
----
|
||||
|
||||
### 如何判断两个class对象是否相同?
|
||||
|
||||
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
|
||||
|
||||
1. 类的完整类名必须一致,包括包名
|
||||
2. **加载这个类的ClassLoader(指ClassLoader实例对象)必须相同**
|
||||
3. 换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
|
||||
|
||||
|
||||
|
||||
### 对类加载器的引用
|
||||
|
||||
1. JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的
|
||||
2. **如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中**
|
||||
3. 当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的(后面讲)
|
||||
|
||||
|
||||
|
||||
408
docs/JVM/JVM系列-第3章-运行时数据区.md
Normal file
408
docs/JVM/JVM系列-第3章-运行时数据区.md
Normal file
@@ -0,0 +1,408 @@
|
||||
---
|
||||
title: JVM系列-第3章-运行时数据区
|
||||
tags:
|
||||
- JVM
|
||||
- 虚拟机
|
||||
categories:
|
||||
- JVM
|
||||
- 1.内存与垃圾回收篇
|
||||
keywords: JVM,虚拟机。
|
||||
description: JVM系列-第3章-运行时数据区。
|
||||
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png'
|
||||
abbrlink: a7ad3cab
|
||||
date: 2020-11-09 15:38:42
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
> 此章把运行时数据区里比较少的地方讲一下。虚拟机栈,堆,方法区这些地方后续再讲。
|
||||
|
||||
运行时数据区概述及线程
|
||||
=================
|
||||
|
||||
前言
|
||||
----
|
||||
|
||||
本节主要讲的是运行时数据区,也就是下图这部分,它是在类加载完成后的阶段
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0001.png">
|
||||
|
||||
当我们通过前面的:类的加载 --> 验证 --> 准备 --> 解析 --\> 初始化,这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0002.png">
|
||||
|
||||
类比一下也就是大厨做饭,我们把大厨后面的东西(切好的菜,刀,调料),比作是运行时数据区。而厨师可以类比于执行引擎,将通过准备的东西进行制作成精美的菜品。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0003.png">
|
||||
|
||||
|
||||
|
||||
运行时数据区结构
|
||||
----------
|
||||
|
||||
### 运行时数据区与内存
|
||||
|
||||
1. 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。**不同的JVM对于内存的划分方式和管理机制存在着部分差异**。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局。
|
||||
|
||||
2. 我们通过磁盘或者网络IO得到的数据,都需要先加载到内存中,然后CPU从内存中获取数据进行读取,也就是说内存充当了CPU和磁盘之间的桥梁
|
||||
|
||||
> 下图来自阿里巴巴手册JDK8
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0004.jpg">
|
||||
|
||||
|
||||
|
||||
### 线程的内存空间
|
||||
|
||||
1. Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区:其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
|
||||
|
||||
2. 灰色的为单独线程私有的,红色的为多个线程共享的。即:
|
||||
- 线程独有:独立包括程序计数器、栈、本地方法栈
|
||||
- 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0005.png">
|
||||
|
||||
|
||||
|
||||
### Runtime类
|
||||
|
||||
**每个JVM只有一个Runtime实例**。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0006.png">
|
||||
|
||||
|
||||
|
||||
线程
|
||||
----
|
||||
|
||||
### JVM 线程
|
||||
|
||||
|
||||
|
||||
1. 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行
|
||||
2. **在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射**
|
||||
- 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收
|
||||
4. 操作系统负责将线程安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法
|
||||
|
||||
> 关于线程,并发可以看笔者的Java并发系列
|
||||
|
||||
|
||||
|
||||
### JVM 系统线程
|
||||
|
||||
|
||||
|
||||
- 如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用`public static void main(String[])`的main线程以及所有这个main线程自己创建的线程。
|
||||
|
||||
- 这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
|
||||
|
||||
1. **虚拟机线程**:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
|
||||
2. **周期任务线程**:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行
|
||||
3. **GC线程**:这种线程对在JVM里不同种类的垃圾收集行为提供了支持
|
||||
4. **编译线程**:这种线程在运行时会将字节码编译成到本地代码
|
||||
5. **信号调度线程**:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理
|
||||
|
||||
|
||||
|
||||
程序计数器(PC寄存器)
|
||||
===========
|
||||
|
||||
PC寄存器介绍
|
||||
----------
|
||||
|
||||
> 官方文档网址:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0007.png">
|
||||
|
||||
1. JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,**寄存器存储指令相关的现场信息**。CPU只有把数据装载到寄存器才能够运行。
|
||||
2. 这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。**JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟**。
|
||||
3. 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
|
||||
4. 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
|
||||
5. 任何时间一个线程都只有一个方法在执行,也就是所谓的**当前方法**。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned)。
|
||||
6. 它是**程序控制流**的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
|
||||
7. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
|
||||
8. 它是**唯一一个**在Java虚拟机规范中没有规定任何OutofMemoryError情况的区域。
|
||||
|
||||
|
||||
|
||||
|
||||
## PC寄存器的作用
|
||||
|
||||
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0008.png">
|
||||
|
||||
|
||||
|
||||
举例
|
||||
------
|
||||
|
||||
```java
|
||||
public class PCRegisterTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
int i = 10;
|
||||
int j = 20;
|
||||
int k = i + j;
|
||||
|
||||
String s = "abc";
|
||||
System.out.println(i);
|
||||
System.out.println(k);
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
查看字节码
|
||||
|
||||
> 看字节码的方法:https://blog.csdn.net/21aspnet/article/details/88351875
|
||||
|
||||
```java
|
||||
Classfile /F:/IDEAWorkSpaceSourceCode/JVMDemo/out/production/chapter04/com/atguigu/java/PCRegisterTest.class
|
||||
Last modified 2020-11-2; size 675 bytes
|
||||
MD5 checksum 53b3ef104479ec9e9b7ce5319e5881d3
|
||||
Compiled from "PCRegisterTest.java"
|
||||
public class com.atguigu.java.PCRegisterTest
|
||||
minor version: 0
|
||||
major version: 52
|
||||
flags: ACC_PUBLIC, ACC_SUPER
|
||||
Constant pool:
|
||||
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
|
||||
#2 = String #27 // abc
|
||||
#3 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
|
||||
#4 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
|
||||
#5 = Class #32 // com/atguigu/java/PCRegisterTest
|
||||
#6 = Class #33 // java/lang/Object
|
||||
#7 = Utf8 <init>
|
||||
#8 = Utf8 ()V
|
||||
#9 = Utf8 Code
|
||||
#10 = Utf8 LineNumberTable
|
||||
#11 = Utf8 LocalVariableTable
|
||||
#12 = Utf8 this
|
||||
#13 = Utf8 Lcom/atguigu/java/PCRegisterTest;
|
||||
#14 = Utf8 main
|
||||
#15 = Utf8 ([Ljava/lang/String;)V
|
||||
#16 = Utf8 args
|
||||
#17 = Utf8 [Ljava/lang/String;
|
||||
#18 = Utf8 i
|
||||
#19 = Utf8 I
|
||||
#20 = Utf8 j
|
||||
#21 = Utf8 k
|
||||
#22 = Utf8 s
|
||||
#23 = Utf8 Ljava/lang/String;
|
||||
#24 = Utf8 SourceFile
|
||||
#25 = Utf8 PCRegisterTest.java
|
||||
#26 = NameAndType #7:#8 // "<init>":()V
|
||||
#27 = Utf8 abc
|
||||
#28 = Class #34 // java/lang/System
|
||||
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
|
||||
#30 = Class #37 // java/io/PrintStream
|
||||
#31 = NameAndType #38:#39 // println:(I)V
|
||||
#32 = Utf8 com/atguigu/java/PCRegisterTest
|
||||
#33 = Utf8 java/lang/Object
|
||||
#34 = Utf8 java/lang/System
|
||||
#35 = Utf8 out
|
||||
#36 = Utf8 Ljava/io/PrintStream;
|
||||
#37 = Utf8 java/io/PrintStream
|
||||
#38 = Utf8 println
|
||||
#39 = Utf8 (I)V
|
||||
{
|
||||
public com.atguigu.java.PCRegisterTest();
|
||||
descriptor: ()V
|
||||
flags: ACC_PUBLIC
|
||||
Code:
|
||||
stack=1, locals=1, args_size=1
|
||||
0: aload_0
|
||||
1: invokespecial #1 // Method java/lang/Object."<init>":()V
|
||||
4: return
|
||||
LineNumberTable:
|
||||
line 7: 0
|
||||
LocalVariableTable:
|
||||
Start Length Slot Name Signature
|
||||
0 5 0 this Lcom/atguigu/java/PCRegisterTest;
|
||||
|
||||
public static void main(java.lang.String[]);
|
||||
descriptor: ([Ljava/lang/String;)V
|
||||
flags: ACC_PUBLIC, ACC_STATIC
|
||||
Code:
|
||||
stack=2, locals=5, args_size=1
|
||||
0: bipush 10
|
||||
2: istore_1
|
||||
3: bipush 20
|
||||
5: istore_2
|
||||
6: iload_1
|
||||
7: iload_2
|
||||
8: iadd
|
||||
9: istore_3
|
||||
10: ldc #2 // String abc
|
||||
12: astore 4
|
||||
14: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
|
||||
17: iload_1
|
||||
18: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
|
||||
21: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
|
||||
24: iload_3
|
||||
25: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
|
||||
28: return
|
||||
LineNumberTable:
|
||||
line 10: 0
|
||||
line 11: 3
|
||||
line 12: 6
|
||||
line 14: 10
|
||||
line 15: 14
|
||||
line 16: 21
|
||||
line 18: 28
|
||||
LocalVariableTable:
|
||||
Start Length Slot Name Signature
|
||||
0 29 0 args [Ljava/lang/String;
|
||||
3 26 1 i I
|
||||
6 23 2 j I
|
||||
10 19 3 k I
|
||||
14 15 4 s Ljava/lang/String;
|
||||
}
|
||||
SourceFile: "PCRegisterTest.java"
|
||||
```
|
||||
|
||||
|
||||
|
||||
* 左边的数字代表**指令地址(指令偏移)**,即 PC 寄存器中可能存储的值,然后执行引擎读取 PC 寄存器中的值,并执行该指令
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0009.png">
|
||||
|
||||
|
||||
|
||||
两个面试题
|
||||
-------
|
||||
|
||||
**使用PC寄存器存储字节码指令地址有什么用呢?**或者问**为什么使用 PC 寄存器来记录当前线程的执行地址呢?**
|
||||
|
||||
1. 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
|
||||
|
||||
2. JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0010.png">
|
||||
|
||||
|
||||
|
||||
**PC寄存器为什么被设定为私有的?**
|
||||
|
||||
1. 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?**为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器**,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
|
||||
2. 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
|
||||
3. 这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
|
||||
|
||||
> 注意并行和并发的区别,笔者的并发系列有讲
|
||||
|
||||
|
||||
|
||||
CPU 时间片
|
||||
---------
|
||||
|
||||
1. CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
|
||||
|
||||
2. 在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
|
||||
|
||||
3. 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,**每个程序轮流执行**。
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0011.png">
|
||||
|
||||
|
||||
|
||||
# 本地方法接口
|
||||
|
||||
## 本地方法
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0012.png">
|
||||
|
||||
|
||||
|
||||
1. 简单地讲,**一个Native Method是一个Java调用非Java代码的接囗**一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern 告知C++编译器去调用一个C的函数。
|
||||
4. “A native method is a Java method whose implementation is provided by non-java code.”(本地方法是一个非Java的方法,它的具体实现是非Java代码的实现)
|
||||
5. 在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
|
||||
6. 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 举例
|
||||
|
||||
|
||||
|
||||
需要注意的是:标识符native可以与其它java标识符连用,但是abstract除外
|
||||
|
||||
```java
|
||||
public class IHaveNatives {
|
||||
public native void Native1(int x);
|
||||
|
||||
public native static long Native2();
|
||||
|
||||
private native synchronized float Native3(Object o);
|
||||
|
||||
native void Native4(int[] ary) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 为什么要使用 Native Method?
|
||||
|
||||
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
|
||||
|
||||
|
||||
|
||||
### 与Java环境外交互
|
||||
|
||||
**有时Java应用需要与Java外面的硬件环境交互,这是本地方法存在的主要原因**。你可以想想Java需要与一些**底层系统**,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
|
||||
|
||||
|
||||
|
||||
### 与操作系统的交互
|
||||
|
||||
1. JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。
|
||||
2. 然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。
|
||||
3. **通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的**。
|
||||
4. 还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
|
||||
|
||||
|
||||
|
||||
### Sun’s Java
|
||||
|
||||
1. Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。
|
||||
2. 例如:类java.lang.Thread的setPriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部在Windows 95的平台上,这个本地方法最终将调用Win32 setpriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。
|
||||
|
||||
|
||||
|
||||
### 本地方法的现状
|
||||
|
||||
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
|
||||
|
||||
|
||||
|
||||
# 本地方法栈
|
||||
|
||||
1. **Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用**。
|
||||
2. 本地方法栈,也是线程私有的。
|
||||
3. 允许被实现成固定或者是可动态扩展的内存大小(在内存溢出方面和虚拟机栈相同)
|
||||
* 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。
|
||||
* 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
|
||||
4. 本地方法一般是使用C语言或C++语言实现的。
|
||||
5. 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/chapter_003/0013.png">
|
||||
|
||||
|
||||
|
||||
**注意事项**
|
||||
|
||||
1. 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
|
||||
* 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
|
||||
* 它甚至可以直接使用本地处理器中的寄存器
|
||||
* 直接从本地内存的堆中分配任意数量的内存
|
||||
2. 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
|
||||
3. 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
|
||||
|
||||
1448
docs/JVM/JVM系列-第4章-虚拟机栈.md
Normal file
1448
docs/JVM/JVM系列-第4章-虚拟机栈.md
Normal file
File diff suppressed because it is too large
Load Diff
1290
docs/JVM/JVM系列-第5章-堆.md
Normal file
1290
docs/JVM/JVM系列-第5章-堆.md
Normal file
File diff suppressed because it is too large
Load Diff
1387
docs/JVM/JVM系列-第6章-方法区.md
Normal file
1387
docs/JVM/JVM系列-第6章-方法区.md
Normal file
File diff suppressed because it is too large
Load Diff
274
docs/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md
Normal file
274
docs/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md
Normal file
@@ -0,0 +1,274 @@
|
||||
---
|
||||
title: JVM系列-第7章-对象的实例化内存布局与访问定位
|
||||
tags:
|
||||
- JVM
|
||||
- 虚拟机
|
||||
categories:
|
||||
- JVM
|
||||
- 1.内存与垃圾回收篇
|
||||
keywords: JVM,虚拟机。
|
||||
description: JVM系列-第7章-对象的实例化内存布局与访问定位。
|
||||
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png'
|
||||
abbrlink: debff71a
|
||||
date: 2020-11-14 19:38:42
|
||||
---
|
||||
|
||||
|
||||
|
||||
对象的实例化内存布局与访问定位
|
||||
======================
|
||||
|
||||
对象的实例化
|
||||
--------
|
||||
|
||||
**大厂面试题**
|
||||
|
||||
美团:
|
||||
|
||||
1. 对象在`JVM`中是怎么存储的?
|
||||
2. 对象头信息里面有哪些东西?
|
||||
|
||||
|
||||
|
||||
蚂蚁金服:
|
||||
|
||||
二面:`java`对象头里有什么
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_007/0001.png">
|
||||
|
||||
### 对象创建的方式
|
||||
|
||||
|
||||
|
||||
1. new:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法
|
||||
2. Class的newInstance方法:在JDK9里面被标记为过时的方法,因为只能调用空参构造器,并且权限必须为 public
|
||||
3. Constructor的newInstance(Xxxx):反射的方式,可以调用空参的,或者带参的构造器
|
||||
4. 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone方法
|
||||
5. 使用序列化:从文件中,从网络中获取一个对象的二进制流,序列化一般用于Socket的网络传输
|
||||
6. 第三方库 Objenesis
|
||||
|
||||
|
||||
|
||||
### 对象创建的步骤
|
||||
|
||||
> **从字节码看待对象的创建过程**
|
||||
|
||||
```java
|
||||
public class ObjectTest {
|
||||
public static void main(String[] args) {
|
||||
Object obj = new Object();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
```
|
||||
public static void main(java.lang.String[]);
|
||||
descriptor: ([Ljava/lang/String;)V
|
||||
flags: ACC_PUBLIC, ACC_STATIC
|
||||
Code:
|
||||
stack=2, locals=2, args_size=1
|
||||
0: new #2 // class java/lang/Object
|
||||
3: dup
|
||||
4: invokespecial #1 // Method java/lang/Object."<init>":()V
|
||||
7: astore_1
|
||||
8: return
|
||||
LineNumberTable:
|
||||
line 9: 0
|
||||
line 10: 8
|
||||
LocalVariableTable:
|
||||
Start Length Slot Name Signature
|
||||
0 9 0 args [Ljava/lang/String;
|
||||
8 1 1 obj Ljava/lang/Object;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**1、判断对象对应的类是否加载、链接、初始化**
|
||||
|
||||
1. 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。
|
||||
2. 如果该类没有加载,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class对象。
|
||||
|
||||
|
||||
|
||||
**2、为对象分配内存**
|
||||
|
||||
1. 首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小
|
||||
2. 如果内存规整:采用指针碰撞分配内存
|
||||
* 如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Point)来为对象分配内存。
|
||||
* 意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往空闲内存那边挪动一段与对象大小相等的距离罢了。
|
||||
* 如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。
|
||||
* 标记压缩(整理)算法会整理内存碎片,堆内存一存对象,另一边为空闲区域
|
||||
3. 如果内存不规整
|
||||
* 如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表来为对象分配内存。
|
||||
* 意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”
|
||||
* 选择哪种分配方式由Java堆是否规整所决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
|
||||
* 标记清除算法清理过后的堆内存,就会存在很多内存碎片。
|
||||
|
||||
|
||||
|
||||
**3、处理并发问题**
|
||||
|
||||
1. 采用CAS+失败重试保证更新的原子性
|
||||
2. 每个线程预先分配TLAB - 通过设置 -XX:+UseTLAB参数来设置(区域加锁机制)
|
||||
3. 在Eden区给每个线程分配一块区域
|
||||
|
||||
|
||||
|
||||
**4、初始化分配到的空间**
|
||||
|
||||
- 所有属性设置默认值,保证对象实例字段在不赋值可以直接使用
|
||||
|
||||
- 给对象属性赋值的顺序:
|
||||
|
||||
1. 属性的默认值初始化
|
||||
2. 显示初始化/代码块初始化(并列关系,谁先谁后看代码编写的顺序)
|
||||
3. 构造器初始化
|
||||
|
||||
|
||||
|
||||
**5、设置对象的对象头**
|
||||
|
||||
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
|
||||
|
||||
|
||||
|
||||
**6、执行init方法进行初始化**
|
||||
|
||||
1. 在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量
|
||||
|
||||
2. 因此一般来说(由字节码中跟随invokespecial指令所决定),new指令之后会接着就是执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。
|
||||
|
||||
|
||||
|
||||
|
||||
> **从字节码角度看 init 方法**
|
||||
|
||||
```java
|
||||
/**
|
||||
* 测试对象实例化的过程
|
||||
* ① 加载类元信息 - ② 为对象分配内存 - ③ 处理并发问题 - ④ 属性的默认初始化(零值初始化)
|
||||
* - ⑤ 设置对象头的信息 - ⑥ 属性的显式初始化、代码块中初始化、构造器中初始化
|
||||
*
|
||||
*
|
||||
* 给对象的属性赋值的操作:
|
||||
* ① 属性的默认初始化 - ② 显式初始化 / ③ 代码块中初始化 - ④ 构造器中初始化
|
||||
*/
|
||||
|
||||
public class Customer{
|
||||
int id = 1001;
|
||||
String name;
|
||||
Account acct;
|
||||
|
||||
{
|
||||
name = "匿名客户";
|
||||
}
|
||||
public Customer(){
|
||||
acct = new Account();
|
||||
}
|
||||
|
||||
}
|
||||
class Account{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**Customer类的字节码**
|
||||
|
||||
```java
|
||||
0 aload_0
|
||||
1 invokespecial #1 <java/lang/Object.<init>>
|
||||
4 aload_0
|
||||
5 sipush 1001
|
||||
8 putfield #2 <com/atguigu/java/Customer.id>
|
||||
11 aload_0
|
||||
12 ldc #3 <匿名客户>
|
||||
14 putfield #4 <com/atguigu/java/Customer.name>
|
||||
17 aload_0
|
||||
18 new #5 <com/atguigu/java/Account>
|
||||
21 dup
|
||||
22 invokespecial #6 <com/atguigu/java/Account.<init>>
|
||||
25 putfield #7 <com/atguigu/java/Customer.acct>
|
||||
28 return
|
||||
```
|
||||
|
||||
|
||||
|
||||
* init() 方法的字节码指令:
|
||||
* 属性的默认值初始化:`id = 1001;`
|
||||
* 显示初始化/代码块初始化:`name = "匿名客户";`
|
||||
* 构造器初始化:`acct = new Account();`
|
||||
|
||||
|
||||
|
||||
|
||||
对象的内存布局
|
||||
---------
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_007/0002.png">
|
||||
|
||||
|
||||
|
||||
> **内存布局总结**
|
||||
|
||||
```java
|
||||
public class Customer{
|
||||
int id = 1001;
|
||||
String name;
|
||||
Account acct;
|
||||
|
||||
{
|
||||
name = "匿名客户";
|
||||
}
|
||||
public Customer(){
|
||||
acct = new Account();
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
Customer cust = new Customer();
|
||||
}
|
||||
}
|
||||
class Account{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
图解内存布局
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_007/0003.png">
|
||||
|
||||
|
||||
|
||||
对象的访问定位
|
||||
---------
|
||||
|
||||
**JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?**
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_007/0004.png">
|
||||
|
||||
定位,通过栈上reference访问
|
||||
|
||||
**对象的两种访问方式:句柄访问和直接指针**
|
||||
|
||||
**1、句柄访问**
|
||||
|
||||
1. 缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低
|
||||
2. 优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_007/0005.png">
|
||||
|
||||
|
||||
|
||||
**2、直接指针(HotSpot采用)**
|
||||
|
||||
1. 优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
|
||||
2. 缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_007/0006.png">
|
||||
535
docs/JVM/JVM系列-第8章-执行引擎.md
Normal file
535
docs/JVM/JVM系列-第8章-执行引擎.md
Normal file
@@ -0,0 +1,535 @@
|
||||
---
|
||||
title: JVM系列-第8章-执行引擎
|
||||
tags:
|
||||
- JVM
|
||||
- 虚拟机
|
||||
categories:
|
||||
- JVM
|
||||
- 1.内存与垃圾回收篇
|
||||
keywords: JVM,虚拟机。
|
||||
description: JVM系列-第8章-执行引擎。
|
||||
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png'
|
||||
abbrlink: 408712f4
|
||||
date: 2020-11-15 19:48:42
|
||||
---
|
||||
|
||||
|
||||
|
||||
执行引擎
|
||||
===========
|
||||
|
||||
执行引擎概述
|
||||
--------
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0001.png">
|
||||
|
||||
### 执行引擎概述
|
||||
|
||||
|
||||
|
||||
1. 执行引擎是Java虚拟机核心的组成部分之一。
|
||||
2. “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而**虚拟机的执行引擎则是由软件自行实现的**,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,**能够执行那些不被硬件直接支持的指令集格式**。
|
||||
3. JVM的主要任务是负责**装载字节码到其内部**,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。
|
||||
4. 那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是**将字节码指令解释/编译为对应平台上的本地机器指令才可以**。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0002.png">
|
||||
|
||||
1、前端编译:从Java程序员-字节码文件的这个过程叫前端编译
|
||||
|
||||
2、执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里的是后端编译)。
|
||||
|
||||
|
||||
|
||||
### 执行引擎工作过程
|
||||
|
||||
> **执行引擎工作过程**
|
||||
|
||||
1. 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
|
||||
2. 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
|
||||
3. 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
|
||||
4. 从外观上来看,所有的Java虚拟机的执行引擎输入、处理、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行、即时编译的等效过程,输出的是执行过程。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0003.png">
|
||||
|
||||
|
||||
|
||||
Java代码编译和执行过程
|
||||
----------------
|
||||
|
||||
### 解释执行和即时编译
|
||||
|
||||
|
||||
|
||||
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过下图中的各个步骤:
|
||||
|
||||
1. 前面橙色部分是编译生成生成字节码文件的过程(javac编译器来完成,也就是前端编译器),和JVM没有关系。
|
||||
|
||||
2. 后面绿色(解释执行)和蓝色(即时编译)才是JVM需要考虑的过程
|
||||
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0004.png">
|
||||
|
||||
|
||||
3. javac编译器(前端编译器)流程图如下所示:
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0005.png">
|
||||
|
||||
|
||||
|
||||
4. Java字节码的执行是由JVM执行引擎来完成,流程图如下所示
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0006.png">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 什么是解释器?什么是JIT编译器?
|
||||
|
||||
1. 解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用**逐行**解释的方式**执行**,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
|
||||
2. JIT(Just In Time Compiler)编译器:就是虚拟机将源代码**一次性直接**编译成和本地机器平台相关的机器语言,**但并不是马上执行**。
|
||||
|
||||
|
||||
|
||||
**为什么Java是半编译半解释型语言?**
|
||||
|
||||
1. JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
|
||||
2. 现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
|
||||
3. JIT编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区的JIT 代码缓存中(执行效率更高了),并且在翻译成本地代码的过程中可以做优化。
|
||||
|
||||
|
||||
|
||||
**用图总结一下**
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0007.png">
|
||||
|
||||
机器码 指令 汇编语言
|
||||
-------------
|
||||
|
||||
### 机器码
|
||||
|
||||
|
||||
|
||||
1. 各种用二进制编码方式表示的指令,叫做机器指令码。开始,人们就用它采编写程序,这就是机器语言。
|
||||
2. 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。
|
||||
3. 用它编写的程序一经输入计算机,CPU直接读取运行,因此和其他语言编的程序相比,执行速度最快。
|
||||
4. 机器指令与CPU紧密相关,所以不同种类的CPU所对应的机器指令也就不同。
|
||||
|
||||
|
||||
|
||||
### 指令和指令集
|
||||
|
||||
**指令**
|
||||
|
||||
1. 由于机器码是由0和1组成的二进制序列,可读性实在太差,于是人们发明了指令。
|
||||
|
||||
2. 指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好
|
||||
|
||||
3. 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同。
|
||||
|
||||
|
||||
|
||||
|
||||
**指令集**
|
||||
|
||||
不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。如常见的
|
||||
|
||||
1. x86指令集,对应的是x86架构的平台
|
||||
2. ARM指令集,对应的是ARM架构的平台
|
||||
|
||||
|
||||
|
||||
### 汇编语言
|
||||
|
||||
|
||||
|
||||
1. 由于指令的可读性还是太差,于是人们又发明了汇编语言。
|
||||
2. 在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。
|
||||
3. 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
|
||||
4. 由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译(汇编)成机器指令码,计算机才能识别和执行。
|
||||
|
||||
|
||||
|
||||
### 高级语言
|
||||
|
||||
|
||||
|
||||
1. 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言
|
||||
|
||||
2. 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0008.png">
|
||||
|
||||
|
||||
|
||||
### 字节码
|
||||
|
||||
|
||||
|
||||
1. 字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码
|
||||
|
||||
2. 字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。
|
||||
|
||||
3. 字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。
|
||||
|
||||
4. 字节码典型的应用为:Java bytecode
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### C、C++源程序执行过程
|
||||
|
||||
**编译过程又可以分成两个阶段:编译和汇编。**
|
||||
|
||||
1. 编译过程:是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码
|
||||
|
||||
2. 汇编过程:实际上指把汇编语言代码翻译成目标机器指令的过程。
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0009.png">
|
||||
|
||||
|
||||
|
||||
解释器
|
||||
-----
|
||||
|
||||
### 为什么要有解释器
|
||||
|
||||
|
||||
|
||||
1. JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法(也就是产生了一个中间产品**字节码**)。
|
||||
|
||||
2. 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
|
||||
|
||||
3. 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0010.png">
|
||||
|
||||
|
||||
|
||||
### 解释器的分类
|
||||
|
||||
|
||||
|
||||
1. 在Java的发展历史里,一共有两套解释执行器,即古老的**字节码解释器**、现在普遍使用的**模板解释器**。
|
||||
* 字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率非常低下。
|
||||
* 而模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。
|
||||
2. 在HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。
|
||||
* Interpreter模块:实现了解释器的核心功能
|
||||
* Code模块:用于管理HotSpot VM在运行时生成的本地机器指令
|
||||
|
||||
### 解释器的现状
|
||||
|
||||
|
||||
|
||||
1. 由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的,比如Python、Perl、Ruby等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些C/C++程序员所调侃。
|
||||
|
||||
2. 为了解决这个问题,JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
|
||||
|
||||
3. 不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。
|
||||
|
||||
|
||||
|
||||
|
||||
## JIT编译器
|
||||
|
||||
|
||||
|
||||
### Java 代码执行的分类
|
||||
|
||||
1. 第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
|
||||
|
||||
2. 第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time)将方法编译成机器码后再执行
|
||||
|
||||
|
||||
|
||||
|
||||
1. HotSpot VM是目前市面上高性能虚拟机的代表作之一。**它采用解释器与即时编译器并存的架构**。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
|
||||
3. 在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++ 程序一较高下的地步。
|
||||
|
||||
|
||||
|
||||
### 为啥我们还需要解释器呢?
|
||||
|
||||
1. 有些开发人员会感觉到诧异,既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
|
||||
|
||||
2. JRockit虚拟机是砍掉了解释器,也就是只采及时编译器。那是因为呢JRockit只部署在服务器上,一般已经有时间让他进行指令编译的过程了,对于响应来说要求不高,等及时编译器的编译完成后,就会提供更好的性能
|
||||
|
||||
|
||||
**首先明确两点:**
|
||||
|
||||
1. 当程序启动后,解释器可以马上发挥作用,**响应速度快**,省去编译的时间,立即执行。
|
||||
2. 编译器要想发挥作用,把代码编译成本地代码,**需要一定的执行时间**,但编译为本地代码后,执行效率高。
|
||||
|
||||
**所以:**
|
||||
|
||||
1. 尽管JRockit VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
|
||||
2. 在此模式下,在Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
|
||||
3. 同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”(后备方案)。
|
||||
|
||||
### 案例
|
||||
|
||||
|
||||
|
||||
- 当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
|
||||
|
||||
|
||||
|
||||
1. 注意解释执行与编译执行在线上环境微妙的辩证关系。**机器在热机状态(已经运行了一段时间叫热机状态)可以承受的负载要大于冷机状态(刚启动的时候叫冷机状态)**。如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无法承载流量而假死。
|
||||
|
||||
2. 在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的1/8。曾经有这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机,此故障说明了JIT的存在。—**阿里团队**
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0011.png">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```java
|
||||
public class JITTest {
|
||||
public static void main(String[] args) {
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
list.add("让天下没有难学的技术");
|
||||
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
通过 JVisualVM 查看 JIT 编译器执行的编译次数
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0012.png">
|
||||
|
||||
|
||||
|
||||
### JIT编译器相关概念
|
||||
|
||||
|
||||
|
||||
1. Java 语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程。
|
||||
2. 也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程。
|
||||
3. 还可能是指使用静态提前编译器(AOT编译器,Ahead of Time Compiler)直接把.java文件编译成本地机器代码的过程。(可能是后续发展的趋势)
|
||||
|
||||
|
||||
|
||||
**典型的编译器:**
|
||||
|
||||
1. 前端编译器:Sun的javac、Eclipse JDT中的增量式编译器(ECJ)。
|
||||
2. JIT编译器:HotSpot VM的C1、C2编译器。
|
||||
3. AOT 编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。
|
||||
|
||||
|
||||
|
||||
### 热点代码及探测方式
|
||||
|
||||
|
||||
|
||||
1. 当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用**执行的频率**而定。
|
||||
2. 关于那些需要被编译为本地代码的字节码,也被称之为**“热点代码”**,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出**深度优化**,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
|
||||
3. 一个被多次调用的方法,或者是一-个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (On StackReplacement)编译。
|
||||
4. 一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。
|
||||
5. **目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测**。
|
||||
6. 采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。
|
||||
1. 方法调用计数器用于统计方法的调用次数
|
||||
2. 回边计数器则用于统计循环体执行的循环次数
|
||||
|
||||
|
||||
|
||||
#### 方法调用计数器
|
||||
|
||||
|
||||
|
||||
1. 这个计数器就用于统计方法被调用的次数,它的默认阀值在Client模式下是1500次,在Server模式下是10000次。超过这个阈值,就会触发JIT编译。
|
||||
|
||||
2. 这个阀值可以通过虚拟机参数 -XX:CompileThreshold 来人为设定。
|
||||
|
||||
3. 当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本
|
||||
|
||||
* 如果存在,则优先使用编译后的本地代码来执行
|
||||
* 如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值。
|
||||
* 如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
|
||||
* 如果未超过阈值,则使用解释器对字节码文件解释执行
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0013.png">
|
||||
|
||||
|
||||
|
||||
#### 热度衰减
|
||||
|
||||
|
||||
|
||||
1. 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即**一段时间之内方法被调用的次数**。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)(半衰周期是化学中的概念,比如出土的文物通过查看C60来获得文物的年龄)
|
||||
2. 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样的话,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
|
||||
6. 另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
|
||||
|
||||
|
||||
|
||||
#### 回边计数器
|
||||
|
||||
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0014.png">
|
||||
|
||||
|
||||
|
||||
### HotSpotVM可以设置程序执行方法
|
||||
|
||||
缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
|
||||
|
||||
1. -Xint:完全采用解释器模式执行程序;
|
||||
2. -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行
|
||||
3. -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
|
||||
|
||||
|
||||
|
||||
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_008/0015.png">
|
||||
|
||||
|
||||
|
||||
**代码测试**
|
||||
|
||||
```java
|
||||
/**
|
||||
* 测试解释器模式和JIT编译模式
|
||||
* -Xint : 6520ms
|
||||
* -Xcomp : 950ms
|
||||
* -Xmixed : 936ms
|
||||
*/
|
||||
public class IntCompTest {
|
||||
public static void main(String[] args) {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
testPrimeNumber(1000000);
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
|
||||
System.out.println("花费的时间为:" + (end - start));
|
||||
|
||||
}
|
||||
|
||||
public static void testPrimeNumber(int count){
|
||||
for (int i = 0; i < count; i++) {
|
||||
//计算100以内的质数
|
||||
label:for(int j = 2;j <= 100;j++){
|
||||
for(int k = 2;k <= Math.sqrt(j);k++){
|
||||
if(j % k == 0){
|
||||
continue label;
|
||||
}
|
||||
}
|
||||
//System.out.println(j);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
结论:只用解释器执行是真的慢
|
||||
|
||||
### HotSpotVM JIT 分类
|
||||
|
||||
|
||||
|
||||
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器 和 C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
|
||||
|
||||
1. -client:指定Java虚拟机运行在Client模式下,并使用C1编译器;
|
||||
* C1编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度。
|
||||
2. -server:指定Java虚拟机运行在server模式下,并使用C2编译器。
|
||||
* C2进行耗时较长的优化,以及激进优化,但优化的代码执行效率更高。(使用C++)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### C1和C2编译器不同的优化策略
|
||||
|
||||
1. 在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、元余消除。
|
||||
* 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
|
||||
* 去虚拟化:对唯一的实现樊进行内联
|
||||
* 冗余消除:在运行期间把一些不会执行的代码折叠掉
|
||||
2. C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:
|
||||
* 标量替换:用标量值代替聚合对象的属性值
|
||||
* 栈上分配:对于未逃逸的对象分配对象在栈而不是堆
|
||||
* 同步消除:清除同步操作,通常指synchronized
|
||||
|
||||
> 也就是说之前的逃逸分析,只有在C2(server模式下)才会触发。那是否说明C1就用不了了?
|
||||
|
||||
### 分层编译策略
|
||||
|
||||
|
||||
|
||||
1. 分层编译(Tiered Compilation)策略:程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
|
||||
|
||||
2. 不过在Java7版本之后,一旦开发人员在程序中显式指定命令“-server"时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。
|
||||
|
||||
|
||||
|
||||
|
||||
1. 一般来讲,JIT编译出来的机器码性能比解释器解释执行的性能高
|
||||
2. C2编译器启动时长比C1慢,系统稳定执行以后,C2编译器执行速度远快于C1编译器
|
||||
|
||||
|
||||
|
||||
#### Graal 编译器
|
||||
|
||||
|
||||
|
||||
* 自JDK10起,HotSpot又加入了一个全新的即时编译器:Graal编译器
|
||||
* 编译效果短短几年时间就追平了G2编译器,未来可期(对应还出现了Graal虚拟机,是有可能替代Hotspot的虚拟机的)
|
||||
* 目前,带着实验状态标签,需要使用开关参数去激活才能使用
|
||||
|
||||
-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompiler
|
||||
|
||||
|
||||
|
||||
|
||||
#### AOT编译器
|
||||
|
||||
|
||||
|
||||
1. jdk9引入了AoT编译器(静态提前编译器,Ahead of Time Compiler)
|
||||
|
||||
2. Java 9引入了实验性AOT编译工具jaotc。它借助了Graal编译器,将所输入的Java类文件转换为机器码,并存放至生成的动态共享库之中。
|
||||
|
||||
3. 所谓AOT编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是**在程序的运行过程中**,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而AOT编译指的则是,**在程序运行之前**,便将字节码转换为机器码的过程。
|
||||
|
||||
.java -> .class -> (使用jaotc) -> .so
|
||||
|
||||
|
||||
|
||||
|
||||
**AOT编译器编译器的优缺点**
|
||||
|
||||
**最大的好处:**
|
||||
|
||||
1. Java虚拟机加载已经预编译成二进制库,可以直接执行。
|
||||
2. 不必等待即时编译器的预热,减少Java应用给人带来“第一次运行慢” 的不良体验
|
||||
|
||||
|
||||
|
||||
**缺点:**
|
||||
|
||||
1. 破坏了 java “ 一次编译,到处运行”,必须为每个不同的硬件,OS编译对应的发行包
|
||||
2. 降低了Java链接过程的动态性,加载的代码在编译器就必须全部已知。
|
||||
3. 还需要继续优化中,最初只支持Linux X64 java base
|
||||
1262
docs/JVM/JVM系列-第9章-StringTable(字符串常量池).md
Normal file
1262
docs/JVM/JVM系列-第9章-StringTable(字符串常量池).md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user