Repository: Swayingleaves/JavaDeveloperBrain
Branch: master
Commit: 9bb89cc659bc
Files: 115
Total size: 1.4 MB
Directory structure:
gitextract_u9dhcn2c/
├── .gitignore
├── 23-Java知识点梳理.xmind
├── Go/
│ └── go.md
├── Java-JVM/
│ ├── JVM的启动过程.md
│ ├── JVM调优.md
│ ├── Java即时编译.md
│ ├── Java对象头.md
│ ├── 内存分配与回收策略.md
│ ├── 内存结构.md
│ ├── 垃圾回收.md
│ └── 类加载机制.md
├── Java-基础/
│ ├── JavaIO.md
│ ├── Java关键字.md
│ ├── Java类型.md
│ ├── Java长期支持版本.md
│ ├── Object.md
│ ├── String.md
│ ├── 反射.md
│ ├── 容器-collection.md
│ ├── 容器-map.md
│ ├── 异常.md
│ ├── 数组.md
│ ├── 泛型.md
│ ├── 继承.md
│ ├── 虚拟线程.md
│ └── 虚拟线程2.md
├── Java-多线程/
│ ├── AQS.md
│ ├── CAS.md
│ ├── Java对象头.md
│ ├── ThreadLocal.md
│ ├── volatile.md
│ ├── 线程.md
│ ├── 线程池.md
│ └── 锁机制.md
├── LICENSE
├── Linux/
│ └── linux.md
├── Mybatis/
│ └── mybatis.md
├── Netty/
│ └── netty.md
├── README.md
├── Redis/
│ ├── Redis.md
│ └── Redis常见面试题.md
├── Spring/
│ ├── Spring.md
│ ├── SpringBoot.md
│ └── SpringMVC.md
├── SpringCloud/
│ └── springcloud.md
├── TODO.md
├── 分布式相关/
│ ├── ApacheSeata.md
│ ├── BASE.md
│ ├── CAP.md
│ ├── 一致性算法.md
│ ├── 分布式ID.md
│ ├── 分布式事务.md
│ └── 分布式锁.md
├── 商城类问题/
│ └── 商城类问题.md
├── 场景设计/
│ └── 场景设计.md
├── 大数据/
│ ├── Flink/
│ │ └── Flink.md
│ ├── Hadoop/
│ │ ├── Hadoop.md
│ │ └── hadoop面试问题.md
│ ├── Hive/
│ │ └── Hive.md
│ └── Spark/
│ └── Spark.md
├── 容器技术/
│ ├── docker.md
│ └── k8s.md
├── 性能测试/
│ └── docker安装配置influxdb-v2+grafana+jemter.md
├── 数据库/
│ ├── Elasticsearch.md
│ ├── Hbase.md
│ ├── MongoDB.md
│ ├── MySQL.md
│ ├── Nebula.md
│ ├── SQL面试题.md
│ └── count.md
├── 数据结构和算法/
│ ├── 01-最长回文子串.md
│ ├── LFU.md
│ ├── LRU.md
│ ├── hotcode.md
│ ├── leetcodetop150.md
│ ├── 二叉树.md
│ ├── 二叉树遍历.md
│ ├── 加减乘除.md
│ └── 排序算法.md
├── 智力题/
│ └── 智力题.md
├── 架构/
│ ├── DDD领域驱动设计.md
│ ├── 系统设计.md
│ └── 计算和储存分离.md
├── 消息队列/
│ ├── Kafka.md
│ ├── Pulsar.md
│ ├── RabbitMQ.md
│ ├── Redis.md
│ ├── RocketMQ.md
│ ├── Zookeeper.md
│ └── mq常见面试题.md
├── 职业规划和学习习惯/
│ └── 职业规划和学习习惯.md
├── 计算机网络/
│ ├── CDN.md
│ ├── HTTP.md
│ ├── HTTP面试题.md
│ ├── IP报文.md
│ ├── JWT.md
│ ├── TCP_IP.md
│ ├── TCP报文.md
│ ├── TLS.md
│ ├── UDP报文.md
│ ├── cookie和session.md
│ ├── 网络协议分层.md
│ ├── 网络攻击行为.md
│ └── 跨域.md
├── 设计模式/
│ ├── 单例模式.md
│ ├── 工厂模式.md
│ ├── 策略模式.md
│ ├── 装饰者模式.md
│ ├── 设计模式面试题.md
│ └── 责任链模式.md
└── 面试解答/
├── HR会问什么.md
├── 面试解答2021-06.md
├── 面试解答2021-07.md
├── 面试解答2021-09.md
└── 面试解答2021-10.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
.idea
================================================
FILE: Go/go.md
================================================
# Go和Java的区别
# go和Java的线程模型区别
# go和Java的GC区别
================================================
FILE: Java-JVM/JVM的启动过程.md
================================================
* [JVM的启动过程](#jvm的启动过程)
* [1、JVM的装入环境和配置](#1jvm的装入环境和配置)
* [2、装载JVM](#2装载jvm)
* [3、初始化JVM,获得本地调用接口](#3初始化jvm获得本地调用接口)
* [4、运行Java程序](#4运行java程序)
# JVM的启动过程
## 1、JVM的装入环境和配置
在学习这个之前,我们需要了解一件事情,就是JDK和JRE的区别。
JDK是面向开发人员使用的SDK,它提供了Java的开发环境和运行环境,JDK中包含了JRE。
JRE是Java的运行环境,是面向所有Java程序的使用者,包括开发者。
JRE = 运行环境 = JVM。
如果安装了JDK,会发现电脑中有两套JRE,一套位于/Java/jre.../下,一套位于/Java/jdk.../jre下。那么问题来了,一台机器上有两套以上JRE,谁来决定运行那一套呢?这个任务就落到java.exe身上,java.exe的任务就是找到合适的JRE来运行java程序。
java.exe按照以下的顺序来选择JRE:
1、自己目录下有没有JRE
2、父目录下有没有JRE
3、查询注册表: HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\"当前JRE版本号"\JavaHome
这几步的主要核心是为了找到JVM的绝对路径。
jvm.cfg的路径为:JRE路径\lib\"CPU架构"\jvm.fig
jvm.cfg的内容大致如下:
-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
KNOWN 表示存在 、IGNORE 表示不存在 、ALIASED_TO 表示给别的JVM去一个别名
WARN 表示不存在时找一个替代 、ERROR 表示不存在抛出异常
## 2、装载JVM
通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。
LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。
## 3、初始化JVM,获得本地调用接口
调用InvocationFunction -> CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。
## 4、运行Java程序
JVM运行Java程序的方式有两种:jar包 与 Class
运行jar 的时候,Java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。
运行Class的时候,main函数直接调用Java.c中的LoadClass方法装载该类。
================================================
FILE: Java-JVM/JVM调优.md
================================================
* [原则](#原则)
* [jvm调优](#jvm调优)
* [JVM调优目标](#jvm调优目标)
* [举例](#举例)
* [JVM调优的步骤](#jvm调优的步骤)
* [JVM参数解析及调优](#jvm参数解析及调优)
# 原则
- 大多数的Java应用不需要进行JVM优化;
- 大多数导致GC问题的原因是代码层面的问题导致的(代码层面);
- 上线之前,应先考虑将机器的JVM参数设置到最优;
- 减少创建对象的数量(代码层面);
- 减少使用全局变量和大对象(代码层面);
- 优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
- 分析GC情况优化代码比优化JVM参数更好(代码层面);
# jvm调优
### JVM调优目标
- 延迟:GC低停顿和GC低频率;
- 低内存占用;
- 高吞吐量;
### 举例
- Heap 内存使用率 <= 70%;
- Old generation内存使用率<= 70%;
- avgpause <= 1秒;
- Full gc 次数0 或 avg pause interval >= 24小时 ;
### JVM调优的步骤
1. 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
2. 确定JVM调优量化目标;
3. 确定JVM调优参数(根据历史JVM参数来调整);
4. 依次调优内存、延迟、吞吐量等指标;
5. 对比观察调优前后的差异;
6. 不断的分析和调整,直到找到合适的JVM参数配置;
7. 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
### JVM参数解析及调优
- -Xmx4g –Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:PermSize=100m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15
- XX:+PrintGCDetails
- -Xlog
- -Xmx4g:堆内存最大值为4GB。
- -Xms4g:初始化堆内存大小为4GB。
- 初始化堆内存大小,默认为物理内存的1/64(小于1GB)。
- -Xmn1200m:设置年轻代大小为1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
- -Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
- -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
- -XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
- -XX:PermSize=100m:初始化永久代大小为100MB。
- -XX:MaxPermSize=256m:设置持久代大小为256MB。
- -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
- -XX:MaxDirectMemorySize=1G:直接内存。报java.lang.OutOfMemoryError: Direct buffer memory异常可以上调这个值。
- -XX:+DisableExplicitGC:禁止运行期显式地调用System.gc()来触发fulll GC。
- -XX:ConcGCThreads=4:CMS垃圾回收器并行线程线,推荐值为CPU核心数。
- -XX:ParallelGCThreads=8:新生代并行收集器的线程数。
- -XX:+HeapDumpOnOutOfMemoryError 当首次遭遇内存溢出时Dump出此时的堆内 ,再使用jhat分析
- 内存优化示例
- java heap:参数-Xms和-Xmx,建议扩大至3-4倍FullGC后的老年代空间占用。
- 永久代:-XX:PermSize和-XX:MaxPermSize,建议扩大至1.2-1.5倍FullGc后的永久带空间占用。
- 新生代:-Xmn,建议扩大至1-1.5倍FullGC之后的老年代空间占用。
- 老年代:2-3倍FullGC后的老年代空间占用。
- 延迟优化示例
- 应用程序可接受的平均停滞时间: 此时间与测量的Minor
- GC持续时间进行比较。可接受的Minor GC频率:Minor
- GC的频率与可容忍的值进行比较。
- 可接受的最大停顿时间:最大停顿时间与最差情况下FullGC的持续时间进行比较。
- 可接受的最大停顿发生的频率:基本就是FullGC的频率。
- 新生代空间越大,Minor GC的GC时间越长,频率越低。如果想减少其持续时长,就需要减少其空间大小。如果想减小其频率,就需要加大其空间大小。
================================================
FILE: Java-JVM/Java即时编译.md
================================================
# 什么是
- 常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由javac编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java通常不如C++这类编译型语言
- 为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In
Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行
- 即时编译器极大地提高了Java程序的运行速度,而且跟静态编译相比,即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。目前,即时编译器已经非常成熟了,在性能层面甚至可以和编译型语言相比。不过在这个领域,大家依然在不断探索如何结合不同的编译方式,使用更加智能的手段来提升程序的运行速度。
# Java的执行过程
## Java的执行过程整体可以分为两个部分
1. 第一步由javac将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译
2. 接下来无需编译直接逐条将字节码解释执行,在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。
- **热点代码** JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能
## 1.JVM中的编译器
JVM中集成了两种编译器
### Client Compiler
- Client Compiler注重启动速度和局部的优化
- HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。C1会做三件事
- 局部简单可靠的优化,比如字节码上进行的一些基础优化,方法内联、常量传播等,放弃许多耗时较长的全局优化。
- 将字节码构造成高级中间表示(High-level Intermediate Representation,以下称为HIR),HIR与平台无关,通常采用图结构,更适合JVM对程序进行优化。
- 最后将HIR转换成低级中间表示(Low-level Intermediate
Representation,以下称为LIR),在LIR的基础上会进行寄存器分配、窥孔优化(局部的优化方式,编译器在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点,通过一些认为可能带来性能提升的转换规则或者通过整体的分析,进行指令转换,来提升代码性能)等操作,最终生成机器码。
### Server Compiler
- Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢
- Server Compiler主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比Client
Compiler高30%以上。目前,Hotspot虚拟机中使用的Server Compiler有两种:C2和Graal
- C2 Compiler
- C2编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为Ideal Graph。 Ideal
Graph表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的那些优化步骤)变得不那么复杂
- Ideal
Graph的构建是在解析字节码的时候,根据字节码中的指令向一个空的Graph中添加节点,Graph中的节点通常对应一个指令块,每个指令块包含多条相关联的指令,JVM会利用一些优化技术对这些指令进行优化,比如Global
Value Numbering、常量折叠等,解析结束后,还会进行一些死代码剔除的操作。生成Ideal
Graph后,会在这个基础上结合收集的程序运行信息来进行一些全局的优化,这个阶段如果JVM判断此时没有全局优化的必要,就会跳过这部分优化。
- 构建Ideal Graph其实就是把指令添加到Ideal Graph,然后形成一个指向这个指令的节点,并且用各种优化手段优化这些指令
- 无论是否进行全局优化,Ideal Graph都会被转化为一种更接近机器层面的MachNode Graph,最后编译的机器码就是从MachNode Graph中得的,生成机器码前还会有一些包括寄存器分配、窥孔优化等操作
- Graal Compiler
- 从JDK 9开始,Hotspot VM中集成了一种新的Server Compiler,Graal编译器。相比C2编译器,Graal有这样几种关键特性
- JVM会在解释执行的时候收集程序运行的各种信息,然后编译器会根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化,所以Graal的峰值性能通常要比C2更好
- 使用Java编写,对于Java语言,尤其是新特性,比如Lambda、Stream等更加友好。
- 更深层次的优化,比如虚函数的内联、部分逃逸分析等
- Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:
+UseJVMCICompiler启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。
## 2.分层编译
### 什么是
- 在Java 7以前,需要研发人员根据服务的性质去选择编译器
- 对于需要快速启动的,或者一些不会长期运行的服务,可以采用编译效率较高的C1,对应参数-client。长期运行的服务,或者对峰值性能有要求的后台服务,可以采用峰值性能更好的C2,对应参数-server
- Java 7开始引入了分层编译的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次
### 五个层级
- 层次
- 解释执行。
- 执行不带profiling的C1代码。
- profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数
- 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。
- 执行带所有profiling的C1代码。
- 执行C2代码。
- 特点
- 通常情况下,C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码,按执行效率排序从高至低则是1层>2层>3层。
- 这5个层次中,1层和4层都是终止状态,当一个方法到达终止状态后,只要编译后的代码并没有失效,那么JVM就不会再次发出该方法的编译请求的。
- 服务实际运行时,JVM会根据服务运行情况,从解释执行开始,选择不同的编译路径,直到到达终止状态
- 常见的编译路径

- 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。
-
如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行。
- 在C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译。
- 前文提到C1中的执行效率是1层>2层>3层,第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。
- 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化。
- 总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。
## 3.即时编译的触发
### Java虚拟机根据方法的调用次数以及循环回边的执行次数来触发即时编译
- 循环回边
- 循环回边是一个控制流图中的概念,程序中可以简单理解为往回跳转的指令,比如下面这段代码:
```java
public void nlp(Object obj) {
int sum = 0;
for (int i = 0; i < 200; i++) {
sum += i;
}
}
```
- 上面这段代码经过编译生成下面的字节码。其中,偏移量为18的字节码将往回跳至偏移量为4的字节码中。在解释执行时,每当运行一次该指令,Java虚拟机便会将该方法的循环回边计数器加1。
- 字节码
```java
public void nlp(java.lang.Object);
Code:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: sipush 200
8: if_icmpge 21
11: iload_1
12: iload_2
13:iadd
14: istore_1
15: iinc 2, 1
18: goto 4
21: return
```
- 在即时编译过程中,编译器会识别循环的头部和尾部。上面这段字节码中,循环体的头部和尾部分别为偏移量为11的字节码和偏移量为15的字节码。编译器将在循环体结尾增加循环回边计数器的代码,来对循环进行计数
当方法的调用次数和循环回边的次数的和,超过由参数`-XX:CompileThreshold`指定的阈值时(使用C1时,默认值为1500;使用C2时,默认值为10000),就会触发即时编译
开启分层编译的情况下,`-XX:CompileThreshold`参数设置的阈值将会失效,触发编译会由以下的条件来判断
- 方法调用次数大于由参数`-XX:TierXInvocationThreshold`指定的阈值乘以系数。
- 方法调用次数大于由参数`-XX:TierXMINInvocationThreshold`指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时。
### 分层编译触发条件公式
- `i >TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s)`i为调用次数,b是循环回边次数
- 上述满足其中一个条件就会触发即时编译,并且JVM会根据当前的编译方法数以及编译线程数动态调整系数s。
# 编译优化
即时编译器会对正在运行的服务进行一系列的优化,包括字节码解析过程中的分析,根据编译过程中代码的一些中间形式来做局部优化,还会根据程序依赖图进行全局优化,最后才会生成机器码
## 1. 中间表达形式(Intermediate Representation)
在编译原理中,通常把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式(Intermediate Representation,以下称为IR),后端会对IR进行优化,生成目标代码
- Java字节码就是一种IR,但是字节码的结构复杂,字节码这样代码形式的IR也不适合做全局的分析优化。现代编译器一般采用图结构的IR,静态单赋值(Static Single Assignment,SSA)IR是目前比较常用的一种。这种IR的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用
- SSA IR
```
Plain Text
{
a = 1;
a = 2;
b = a;
}
```
- 上述代码中我们可以轻易地发现a = 1的赋值是冗余的,但是编译器不能。传统的编译器需要借助数据流分析,从后至前依次确认哪些变量的值被覆盖掉。不过,如果借助了SSA IR,编译器则可以很容易识别冗余赋值。
- 上面代码的SSA IR形式的伪代码可以表示为
```Plain Text
{
a_1 = 1;
a_2 = 2;
b_1 = a_2;
}
```
- 由于SSA IR中每个变量只能赋值一次,所以代码中的a在SSA IR中会分成a_1、a_2两个变量来赋值,这样编译器就可以很容易通过扫描这些变量来发现a_1的赋值后并没有使用,赋值是冗余的
- 我们可以将编译器的每一种优化看成一个图优化算法,它接收一个IR图,并输出经过转换后的IR图。编译器优化的过程就是一个个图节点的优化串联起来的
## 2.方法内联
- 方法内联,是指在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。JIT大部分的优化都是在内联的基础上进行的,方法内联是即时编译器中非常重要的一环。
- Java服务中存在大量getter/setter方法,如果没有方法内联,在调用getter/setter时,程序执行时需要保存当前方法的执行位置,创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。内联了对 getter/setter的方法调用后,上述操作仅剩字段访问
- 编译器的大部分优化都是在方法内联的基础上。所以一般来说,内联的方法越多,生成代码的执行效率越高。但是对于即时编译器来说,内联的方法越多,编译时间也就越长,程序达到峰值性能的时刻也就比较晚
### 虚函数内联(多态、接口)
内联是JIT提升性能的主要手段,但是虚函数使得内联是很难的,因为在内联阶段并不知道他们会调用哪个方法。例如,我们有一个数据处理的接口,这个接口中的一个方法有三种实现add、sub和multi,JVM是通过保存虚函数表Virtual Method Table(以下称为VMT)存储class对象中所有的虚函数,class的实例对象保存着一个VMT的指针,程序运行时首先加载实例对象,然后通过实例对象找到VMT,通过VMT找到对应方法的地址,所以虚函数的调用比直接指向方法地址的classic call性能上会差一些。很不幸的是,Java中所有非私有的成员函数的调用都是虚调用
- 比如如下代码将会优化
```java
public class SimpleInliningTest {
public static void main(String[] args) throws InterruptedException {
VirtualInvokeTest obj = new VirtualInvokeTest();
VirtualInvoke1 obj1 = new VirtualInvoke1();
for (int i = 0; i < 100000; i++) {
invokeMethod(obj);
invokeMethod(obj1);
}
Thread.sleep(1000);
}
public static void invokeMethod(VirtualInvokeTest obj) {
obj.methodCall();
}
private static class VirtualInvokeTest {
public void methodCall() {
System.out.println("virtual call");
}
}
private static class VirtualInvoke1 extends VirtualInvokeTest {
@Override
public void methodCall() {
super.methodCall();
}
}
}
```
- 如下代码多个实现未被优化
```java
public class SimpleInliningTest {
public static void main(String[] args) throws InterruptedException {
VirtualInvokeTest obj = new VirtualInvokeTest();
VirtualInvoke1 obj1 = new VirtualInvoke1();
VirtualInvoke2 obj2 = new VirtualInvoke2();
for (int i = 0; i < 100000; i++) {
invokeMethod(obj);
invokeMethod(obj1);
invokeMethod(obj2);
}
Thread.sleep(1000);
}
public static void invokeMethod(VirtualInvokeTest obj) {
obj.methodCall();
}
private static class VirtualInvokeTest {
public void methodCall() {
System.out.println("virtual call");
}
}
private static class VirtualInvoke1 extends VirtualInvokeTest {
@Override
public void methodCall() {
super.methodCall();
}
}
private static class VirtualInvoke2 extends VirtualInvokeTest {
@Override
public void methodCall() {
super.methodCall();
}
}
}
```
## 3. 逃逸分析
逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。Java虚拟机的即时编译器会对新建的对象进行逃逸分析,判断对象是否逃逸出线程或者方法。即时编译器判断对象是否逃逸的依据有两种
- 对象是否被存入堆中(静态字段或者堆中对象的实例字段),一旦对象被存入堆中,其他线程便能获得该对象的引用,即时编译器就无法追踪所有使用该对象的代码位置。
- 对象是否被传入未知代码中,即时编译器会将未被内联的代码当成未知代码,因为它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中,这种情况,可以直接认为方法调用的调用者以及参数是逃逸的。
### 1、锁消除
- 在学习Java并发编程时会了解锁消除,而锁消除就是在逃逸分析的基础上进行的。
- 如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没就有意义。因为线程并不能获得该锁对象。在这种情况下,即时编译器会消除对该不逃逸锁对象的加锁、解锁操作。实际上,编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。由于Java虚拟机即时编译的限制,上述条件被强化为证明锁对象不逃逸出当前编译的方法。不过,基于逃逸分析的锁消除实际上并不多见。
### 2、栈上分配
- 我们都知道Java的对象是在堆上分配的,而堆是对所有对象可见的。同时,JVM需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存
- 如果逃逸分析能够证明某些新建的对象不逃逸,那么JVM完全可以将其分配至栈上,并且在new语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象
### 3、标量替换
过Hotspot虚拟机,并没有进行实际的栈上分配,而是使用了标量替换这一技术。所谓的标量,就是仅能存储一个值的变量,比如Java代码中的基本类型。与之相反,聚合量则可能同时存储多个值,其中一个典型的例子便是Java的对象。编译器会在方法内将未逃逸的聚合量分解成多个标量,以此来减少堆上分配。下面是一个标量替换的例子
```java
public class Example{
@AllArgsConstructor
class Cat{
int age;
int weight;
}
public static void example(){
Cat cat = new Cat(1,10);
addAgeAndWeight(cat.age,Cat.weight);
}
}
```
- 经过逃逸分析,cat对象未逃逸出example()的调用,因此可以对聚合量cat进行分解,得到两个标量age和weight,进行标量替换后的伪代码:
```java
public class Example{
@AllArgsConstructor
class Cat{
int age;
int weight;
}
public static void example(){
int age = 1;
int weight = 10;
addAgeAndWeight(age,weight);
}
}
```
### 4、部分逃逸分析
部分逃逸分析也是Graal对于概率预测的应用。通常来说,如果发现一个对象逃逸出了方法或者线程,JVM就不会去进行优化,但是Graal编译器依然会去分析当前程序的执行路径,它会在逃逸分析基础上收集、判断哪些路径上对象会逃逸,哪些不会。然后根据这些信息,在不会逃逸的路径上进行锁消除、栈上分配这些优化手段。
## 4. Loop Transformations
C2编译器在构建Ideal Graph后会进行很多的全局优化,其中就包括对循环的转换,最重要的两种转换就是循环展开和循环分离
### 1、循环展开
循环展开是一种循环转换技术,它试图以牺牲程序二进制码大小为代价来优化程序的执行速度,是一种用空间换时间的优化手段。
- 循环展开通过减少或消除控制程序循环的指令,来减少计算开销,这种开销包括增加指向数组中下一个索引或者指令的指针算数等。如果编译器可以提前计算这些索引,并且构建到机器代码指令中,那么程序运行时就可以不必进行这种计算。也就是说有些循环可以写成一些重复独立的代码。比如下面这个循环
```java
public void loopRolling(){
for(int i = 0;i<200;i++){
delete(i);
}
}
```
- 上面的代码需要循环删除200次,通过循环展开可以得到下面这段代码:
```java
public void loopRolling(){
for(int i = 0;i<200;i+=5){
delete(i);
delete(i+1);
delete(i+2);
delete(i+3);
delete(i+4);
}
}
```
- 这样展开就可以减少循环的次数,每次循环内的计算也可以利用CPU的流水线提升效率。当然这只是一个示例,实际进行展开时,JVM会去评估展开带来的收益,再决定是否进行展开。
### 2、循环分离
循环分离也是循环转换的一种手段。它把循环中一次或多次的特殊迭代分离出来,在循环外执行。举个例子,下面这段代码
```java
int a = 10;
for(int i = 0;i<10;i++){
b[i] = x[i] + x[a];
a = i;
}
```
可以看出这段代码除了第一次循环a = 10以外,其他的情况a都等于i-1。所以可以把特殊情况分离出去,变成下面这段代码
```java
b[0] = x[0] + 10;
for(int i = 1;i<10;i++){
b[i] = x[i] + x[i-1];
}
```
这种等效的转换消除了在循环中对a变量的需求,从而减少了开销。
## 5. 窥孔优化与寄存器分配
前文提到的窥孔优化是优化的最后一步,这之后就会程序就会转换成机器码,窥孔优化就是将编译器所生成的中间代码(或目标代码)中相邻指令,将其中的某些组合替换为效率更高的指令组,常见的比如强度削减、常数合并等,看下面这个例子就是一个强度削减的例子
- 强度削减
- y1=x1*3 经过强度削减后得到 y1=(x1<<1)+x1
- 编译器使用移位和加法削减乘法的强度,使用更高效率的指令组
- 寄存器分配
- 寄存器分配也是一种编译的优化手段,在C2编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中,CPU访问寄存器的速度比内存快得多,可以提升程序的运行速度。
- 寄存器分配和窥孔优化是程序优化的最后一步。经过寄存器分配和窥孔优化之后,程序就会被转换成机器码保存在codeCache中
# 参考文章
- https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html
================================================
FILE: Java-JVM/Java对象头.md
================================================
# Java对象头
由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。
# 对象头形式
JVM中对象头的方式有以下两种(以32位JVM为例):
## 普通对象
```text
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
```
## 数组对象
```text
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
```
# 对象头的组成
## Mark Word
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
```text
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
```
其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
|biased_lock| lock| 状态|
|:----------|:----|:-----|
|0| 01 |无锁|
|1| 01 |偏向锁|
|0| 00 |轻量级锁|
|0| 10 |重量级锁|
|0| 11 |GC标记|
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
- age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
- identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
- thread:持有偏向锁的线程ID。
- epoch:偏向时间戳。
- ptr_to_lock_record:指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:指向管程Monitor的指针。
64位下的标记字与32位的相似,不再赘述:
```text
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|------------------------------------------------------------------------------|--------------------|
```
## class pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
1. 每个Class的属性指针(即静态变量)
2. 每个对象的属性指针(即对象变量)
3. 普通对象数组的每个元素指针
## array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
# 参考文章
- https://www.jianshu.com/p/3d38cba67f8b
================================================
FILE: Java-JVM/内存分配与回收策略.md
================================================
* [对象优先在 Eden 分配](#对象优先在-eden-分配)
* [大对象直接进入老年代](#大对象直接进入老年代)
* [长期存活的对象进入老年代](#长期存活的对象进入老年代)
* [动态对象年龄判定](#动态对象年龄判定)
* [空间分配担保](#空间分配担保)
### 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
### 大对象直接进入老年代
- 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
- 经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象
- -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
### 长期存活的对象进入老年代
- 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
- -XX:MaxTenuringThreshold 用来定义年龄的阈值
### 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄。
### 空间分配担保
- 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
- 如果不成立的话则虚拟机会先查看- XX:HandlePromotionFailure参数的设置值是否允许担保失败,如果允许那么就会继续检查老年代 最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那么就要进行一次 Full GC。
- 解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率, 只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无 法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代 本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之 前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与 老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
================================================
FILE: Java-JVM/内存结构.md
================================================
# 内存结构
## JDK1.7和1.8对比

# 程序计数器
- 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器
- 字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令
# Java虚拟机栈
虚拟机栈描述的是Java方法执行的线程内存模型:
- 每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息
- 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK1.5+ 默认为 1M:
- java -Xss2M HackTheJava
- 异常
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
# 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
# 堆
“几乎”所有的对象实例都在这里分配内存
## 堆的分代(g1前)
- 新生代 Eden:Survivor=8:1:1
- Eden
- Survivor
- to
- from
- 老年代
- JVM参数 新生代:老年代=1:2
- -Xms 初始值
- -Xmx 最大值
## 新的堆内存结构
G1 之前的 JVM 内存模型
G1收集器的内存模型
堆内存会被切分成为很多个固定大小区域(Region),每个是连续范围的虚拟内存。
堆内存中一个区域 (Region) 的大小,可以通过 -XX:G1HeapRegionSize 参数指定,大小区间最小 1M 、最大 32M ,总之是 2 的幂次方。
默认是将堆内存按照 2048 份均分。
每个 Region 被标记了 E、S、O 和 H,这些区域在逻辑上被映射为 Eden,Survivor 和老年代。
存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程。如上图所示,区域可以分配到 Eden,survivor 和老年代。
此外,还有第四种类型,被称为巨型区域(Humongous Region)。
Humongous 区域主要是为存储超过 50% 标准 region 大小的对象设计,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC 。
## 逃逸分析
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象动态作用域
- 举例
- 方法逃逸
- 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸
- 代码
- ```java
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
```
- StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部
- 上述代码如果想要StringBuffer sb不逃出方法,可以这样写
- ```java
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
```
- 不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法
- 线程逃逸
- 甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸
- 使用逃逸分析,编译器可以对代码做如下优化
- 一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- JVM参数
- -XX:+DoEscapeAnalysis : 表示开启逃逸分析
- -XX:-DoEscapeAnalysis : 表示关闭逃逸分析
- 对象的栈上内存分配
- 我们知道,在一般情况下,对象和数组元素的内存分配是在堆内存上进行的。但是随着JIT编译器的日渐成熟,很多优化使这种分配策略并不绝对,JIT编译器就可以在编译期间根据逃逸分析的结果,来决定是否可以将对象的内存分配从堆转化为栈
- 三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
# 方法区
## 1.7
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK
## 1.8
开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
## 方法区的回收
- 因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高
- 主要是对常量池的回收和对类的卸载
- 为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能
- 类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
# 运行时常量池
运行时常量池是方法区的一部分。
- Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
- 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
# 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分
- 在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。
- 受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制
# 参考文章
- https://www.hollischuang.com/archives/2398
================================================
FILE: Java-JVM/垃圾回收.md
================================================
# 判断一个对象能否被回收
## 引用计数法
- 为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
- 在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
## 可达性算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
## 哪些可以作为是根节点
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 正在运⾏的线程
- 锁住的对象
这种算法的优点是简单、高效,但缺点是有可能出现"内存泄漏",即存在一些对象已经无用了,但是由于这些对象与GC Roots对象之间存在间接引用,导致这些对象不能被回收。
除了可达性分析算法外,还有一些高级的垃圾回收算法,如G1垃圾收集器的Region To Space算法,以及ZGC垃圾收集器的读屏障算法等。这些算法都能够更加精确地判断对象是否可以被回收,以及更加高效地进行垃圾回收。
## 方法区的回收
## finalize()
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
- 经过可达性分析后如果对象没有域root有任何相关的引用链,则会被标记,随后经过一次筛选,筛选的条件就是是否需要执行finalize()方法,如果对象没有覆盖finalize方法,或者已经被虚拟机调用过。则没必要执行
- 如果需要执行,则把对象放在一个名为F-Queue的队列中中,并由一个优先级低的由虚拟机创建的线程去执行他们的finalize()方法,这里的执行只是触发,并不一定要等待他们执行完,否则如果该方法之心缓慢甚至死循环,则会阻塞队列
## 引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
引用类型的种类
### 强引用
- 被强引用关联的对象不会被回收。
- 使用 new 一个新对象的方式来创建强引用。`Object obj = new Object();`
### 软引用
- 被软引用关联的对象只有在内存不够的情况下才会被回收。
- 使用 SoftReference 类来创建软引用。
```java
Object obj = new Object();
SoftReference