首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
前言
前段时间翻看自己多年以来攒下的满满家当 , 突然有一种满满的满足感 .
但是想想多年来找资料的艰辛 , 决定将这些文档整理出来, 分享给大家 .
笔记华而不实 , 其中可能也有不正确的地方 , 欢迎指正.
在此也感谢道友们的奉献 , 文档暂分为几个:
另外还有其他的笔记会陆陆续续的分享处理 , 谢谢大家的支持 .
一 . 基础知识
1.1 内存溢出
1.2 系统的线程划分
1 | java复制代码// 串行收集器 |
1.3 Java 的四种引用类型
- 强引用 (StrongReference) : 通常为 new 直接创建 , 只要还有对象指向 , 就不会发生回收
- 软引用(SoftReference): 内存溢出之前会 clear 弱引用 , 如果还是存在 , 则抛出OutofMemory (extends WeakReference)
- 弱引用(WeakReference): 只会生存到下一次生命周期之前 , SoftReference (new SoftReference(res))
- 虚引用(PhantomReference): 主要作用是在垃圾收集时拿到一个通知
1 | java复制代码// 软引用创建案例一 : |
1.4 内存分配的方式
内存分配有2种方式 :
- 指针碰撞 : 内存规整 , 移动指针切换分配内存
- 空闲列表 : 维护列表记录哪些内存可用 , 从列表中获取
1.5 TLAB
什么叫 TLAB ?
TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。 如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
在代码流程中 , 对象通过引用指向实际的内存空间 , 而指向的即为对应的指针
在堆内存中 , 一片内存被一个指针一分为2 , 左边为已经分配内存的空间,右侧为空 , 每一次有新的对象创建,指针就会向右移动一个对象size的距离 , 这就被称为指针碰撞。但是当多线程高并发情况下 , 会出现指针来不及修改的情况
TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项 - XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比大小。
TLAB的本质其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。
TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。
TLAB 的缺陷
- TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)
- TLAB空间还剩一点点没有用到,有点舍不得。
- Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC,
- TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理。
二 . 虚拟机
2 . 1 Java 虚拟机
Java 虚拟机,是一个可以执行 Java 字节码的虚拟机进程 , 它允许Java 查询在多个任意平台使用 , 但是跨平台的是 Java 程序(包括字节码文件) , 而不是 JVM
1 | java复制代码// 内存区 |
2 . 2 内存堆细节
1 | java复制代码// 内存堆特点 |
2 . 3 内存栈细节
- 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。
- 存放基本数据类型以及对象的引用。
- 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。
- 方法栈帧是以先进后出的方式虚拟机栈的。每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。
- 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
2 . 4 Java 内存堆和栈区别
- 栈内存 : 用来存储基本类型的变量和对象的引用变量
- 堆内存 : 用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存;
- 堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
- 如果栈内存没有可用的空间存储方法调用和局部变量,JVM 会抛出 java.lang.StackOverFlowError 错- 误;
- 如果是堆内存没有可用的空间存储生成的对象,JVM 会抛出 java.lang.OutOfMemoryError 错误。
栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满。-Xss 选项设置栈内存的大小,-Xms 选项可以设置堆的开始时的大小。
2 . 6 HotSpot虚拟机
HotSpot 虚拟机将其物理上分为了2个部分 :
新生代(young generation)
- 绝大多数最新被创建的对象会被分配到这里
- 对象从这个区域消失的过程我们称之为”minor GC“
- 新生代三空间
- 一个伊甸园空间(Eden )
- 两个幸存者空间(Survivor )
伊甸园Survivor-ASurvivor-BOld对象创建第一次GC对象不断累积根据算法移动到第二空间继续累计 ,GC几轮后剩下的放入老年代伊甸园Survivor-ASurvivor-BOld
对象描述
1 | java复制代码// 老年代(old generation) |
加快缓存分配的方式
- bump-the-pointer
- 跟踪在伊甸园空间创建的最后一个对象 ,放在顶部,下次创建查找该对象
- TLABs(Thread-Local Allocation Buffers)
- 该方案为每一个线程在伊甸园空间分配一块独享的空间,这样每个线程只访问他们自己的TLAB空间,再与bump-the-pointer技术结合可以在不加锁的情况下分配内存
三 . 垃圾清理
3 . 1 垃圾回收的起因
程序员无法自动完成系统的GC ,GC 一般在以下环境被创建
- 大多数对象会很快变得不可达
- 只有很少的由老对象(创建时间较长的对象)指向新生对象的引用
3 . 2 垃圾回收中的概念
1 | java复制代码// stop-the-world |
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
设置垃圾收集器
1 | java复制代码XX:+UseSerialGC:设置串行收集器 |
设置垃圾收集器的大小
1 | java复制代码-XX:NewSize:设置年轻代最小空间大小 |
3 . 3 常见的垃圾收集器
3 .3 .1 Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器,它在进行垃圾收集的时候会暂停所有的工作线程,直到完成垃圾收集过程。下面是Serial垃圾收集器的运行示意图:
3 .3 .2 ParNew 收集器
1 | arduino复制代码ParNew 垃圾收集器实则是Serial 垃圾收集器的多线程版本,这个多线程在于ParNew垃圾收集器可以使用多条线程进行垃圾回收。 |
3 .3 .3 Parallel Scavenge 收集器
Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为 吞吐量收集器
目的 : 即减少垃圾收集时间(就是每次垃圾收集时间短,但是收集次数多),让用户代码获得更长的运行时间
3. 3 .4 Serial Old 收集器
1 | java复制代码Serial Old 收集器是Serial 收集器的老年代版本。其垃圾收集器的运行原理和Serial 收集器是一样的。 |
3 .3 .5 Parallel Old 收集器
1 | java复制代码Parallel Old 收集器同样是Parallel Scavenge 收集器的老年代版本,支持多线程并发收集。 |
3 .3 .6 CMS 收集器
1 | java复制代码CMS 垃圾收集器的运作过程相对前面几个垃圾收集器来说比较复杂,整个过程可以分为四个部分: |
3 .3 .7 Garbage First 收集器
Garbage First(简称G 1)收集器是垃圾收集器发展史上里程碑式的成果,主要面向服务端应用程序。另外G 1收集器虽然还保留新生代和老年代的概念,但是新生代和老年代不在固定,它们都是一系列区域的动态集合。
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的就对象以获取更好的收集效果。
- 空间整合:G1从整体上来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。
- 可预测的停顿:这是G1相对于CMS的另一大优势。
3 .4 常见得垃圾回收方式
1 | java复制代码// 方式一 : 调用 system gc 方法 , 开发者手动调用该命令 , 触发 gc |
3 . 5 垃圾回收的算法
1 | java复制代码// > 应用计数 |
四 . 对象的创建
4 . 1 创建过程
1)检测类是否被加载
当虚拟机遇到 new 指令时,首先先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就执行类加载过程。
2)为对象分配内存
类加载完成以后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了。只需要在堆上分配所需要的内存即可。
具体的分配内存有两种情况:
- 第一种情况是内存空间绝对规整
- 第二种情况是内存空间是不连续的。
- 对于内存绝对规整的情况相对简单一些,虚拟机只需要在被占用的内存和可用空间之间移动指针即可,这种方式被称为“指针碰撞”。
- 对于内存不规整的情况稍微复杂一点,这时候虚拟机需要维护一个列表,来记录哪些内存是可用的。分配内存的时候需要找到一个可用的内存空间,然后在列表上记录下已被分配,这种方式成为“空闲列表”。
多线程并发时会出现正在给对象 A 分配内存,还没来得及修改指针,对象 B 又用这个指针分配内存,这样就出现问题了。解决这种问题有两种方案:
- 第一种,是采用同步的办法,使用 CAS 来保证操作的原子性。
- 另一种,是每个线程分配内存都在自己的空间内进行,即是每个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),分配内存的时候再TLAB上分配,互不干扰。可以通过 -XX:+/-UseTLAB 参数决定。
3)为分配的内存空间初始化零值
对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。
4)对对象进行其他设置
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的 hashcode ,GC 分代年龄等信息。
5)执行 init 方法
执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于 Java 程序来说还需要执行 init 方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了 init 方法之后,这个对象才真正能使用。
4 . 2 内存布局
1 | java复制代码对象的内存布局包括三个部分: |
4 . 3 对象的访问定位
1 | java复制代码句柄定位:Java 堆会画出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 |
4 . 4 对象死亡
引用计数算法: 为对象添加一个引用计数器,每当对象在一个地方被引用,则该计数器加1;每当对象引用失效时,计数器减1。但计数器为0的时候,就表白该对象没有被引用。
可达性分析算法: 通过一系列被称之为“GC Roots”的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收。
// GC Root 对象 : 可达性的根对象
Java虚拟机栈中被引用的对象,各个线程调用的参数、局部变量、临时变量等。
方法区中类静态属性引用的对象,比如引用类型的静态变量。
方法区中常量引用的对象。本地方法栈中所引用的对象。
Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。
被同步锁(synchronized)持有的对象。
4 . 6 类加载器
什么是类加载器
类加载器(ClassLoader),用来加载 Java 类到 Java 虚拟机中 , 一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件) , 类加载器,负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例
发生的时期
- 1、遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类还没进行初始化,则需要先触发其初始化。
- 2、使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类还没进行初始化,则需要先触发其初始化。
- 3、当初始化了一个类的时候,如果发现其父类还没进行初始化,则需要先触发其父类的初始化。
- 4、当虚拟机启动时,用户需要指定一个执行的主类,即调用其 #main(String[] args) 方法,虚拟机则会先初始化该主类。
- 5、当使用 JDK7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
加载Class 的方式
- 第一个阶段,加载(Loading),是找到 .class 文件并把这个文件包含的字节码加载到内存中。
- 第二阶段,连接(Linking),又可以分为三个步骤,分别是字节码验证、Class 类数据结构分析及相应的内存分配、最后的符号表的解析。
- 第三阶段,Initialization(类中静态属性和初始化赋值),以及Using(静态块的执行)等。
4 . 7 ClassLoader 详解
1 | java复制代码// Java 中有三个类加载器 |
1 | java复制代码// 双亲委派 |
4 . 8 class 文件
1 | java复制代码// Class 加载流程 |
Java 对象头的存储结构 32 位 TODO : 待完善
好文推荐@ 从一个class文件深入理解Java字节码结构_四月葡萄的博客-CSDN博客_java字节码
查看字节码的方式
1 | java复制代码// 方法一 : Java 基本工具类 |
4 . 9 对象在JVM 中的表示 – OOP-Klass
1 | java复制代码HotSpot 通过 OOP-Klass 模型来在虚拟机中表示一个对象 , 这里的 OOP 指的是 Ordinary Object Pointer (普通对象指针),它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象。而 Klass 则包含元数据和方法信息,用来描述Java类。 |
五 . GC 监控
5 .1 什么时 GC 监控
1 | java复制代码GC 监控是指监控 JVM 执行 GC 的过程 |
5 .2 常见的 GC 监控工具
1 | java复制代码• jps :虚拟机进程状况工具 |
5.3 监控常用命令
1 | java复制代码// 获取 Java 程序使用的内存 |
5.4 GC 分析方式
1 | java复制代码// --------------- jconsole 使用 |
5.5 压测工具扩展
1 | java复制代码// 常用的压测工具 |
5.6 Jstack 使用
1 | java复制代码> Step 1 : 拿到 pid |
5.7 JOL 使用
1 | java复制代码JOL:查看Java 对象布局、大小工具 |
5.8 Thread Dump
1 | java复制代码Thread Dump是非常有用的诊断Java应用问题的工具。每一个Java虚拟机都有及时生成所有线程在某一点状态的thread-dump的能力,虽然各个 Java虚拟机打印的thread dump略有不同,但是大多都提供了当前活动线程的快照,及JVM中所有Java线程的堆栈跟踪信息,堆栈信息一般包含完整的类名及所执行的方法,如果可能的话还有源代码的行数。 |
5.9 GC日志详情
1 | java复制代码// 使用方法 : |
六 . GC 优化
6 . 1 GC 优化的前提
1 | java复制代码> GC 优化永远是最后一项任务 |
6 . 2 GC 优化的方案
1 | java复制代码> 使用 StringBuilder 或者StringBuffer 来替代String |
6 . 3 GC 优化需要考虑的参数
6 . 4 GC类型可选参数
6 . 5 GC 优化过程
1 | java复制代码1 > 监控 GC 状态 |
6 . 6 内存溢出的情况及分析
1 | java复制代码> 1 堆栈溢出 |
6 . 7 Full GC 原因分析及解决
1 | java复制代码// 原因 : |
七 小知识点
7 . 1 Jar 相关
1 | java复制代码TODO |
7 . 2 CPU
1 | java复制代码TODO |
八 其他
# 32 位 JVM 和 64 位 JVM 的最大堆内存分别是多少
1 | java复制代码理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB |
# 直接内存(堆外内存)
1 | java复制代码> 直接内存(Direct Memory),并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中农定义的内存区域 |
# 其他工具
1 | java复制代码> GCEasy : |
# JMC 分析流程
1 | java复制代码// Step 1 : 开启指定应用的飞行记录 |
# skywalking 分析流程
1 | java复制代码skywalking 是链路分析工具 , 是很好的辅助工具 , 能快速的分析瓶颈点 |
九 优化的流程
9.1 压测流程
1 | java复制代码> 对连接数进行优化 , 包括 |
附录 :
常见的 JVM 参数
Jstat 参数名称
描述 | 参数名称 |
---|---|
输出每个堆区域的当前可用空间以及已用空间(伊甸园,幸存者等等),GC执行的总次数,GC操作累计所花费的时间。 | gc |
输出每个堆区域的最小空间限制(ms)/最大空间限制(mx),当前大小,每个区域之上执行GC的次数。(不输出当前已用空间以及GC执行时间)。 | gccapactiy |
输出-gcutil提供的信息以及最后一次执行GC的发生原因和当前所执行的GC的发生原因 | gccause |
输出新生代空间的GC性能数据 | gcnew |
输出新生代空间的大小的统计数据。 | gcnewcapacity |
输出老年代空间的GC性能数据。 | gcold |
输出老年代空间的大小的统计数据。 | gcoldcapacity |
输出持久带空间的大小的统计数据。 | gcpermcapacity |
输出每个堆区域使用占比,以及GC执行的总次数和GC操作所花费的事件。 | gcutil |
列 | 说明 | Jstat参数 |
---|---|---|
S0C | 输出Survivor0空间的大小。单位KB。 | -gc -gccapacity -gcnew -gcnewcapacity |
S1C | 输出Survivor1空间的大小。单位KB。 | -gc -gccapacity -gcnew -gcnewcapacity |
S0U | 输出Survivor0已用空间的大小。单位KB。 | -gc -gcnew |
S1U | 输出Survivor1已用空间的大小。单位KB。 | -gc -gcnew |
EC | 输出Eden空间的大小。单位KB。 | -gc -gccapacity -gcnew -gcnewcapacity |
EU | 输出Eden已用空间的大小。单位KB。 | -gc -gcnew |
OC | 输出老年代空间的大小。单位KB。 | -gc -gccapacity -gcold -gcoldcapacity |
OU | 输出老年代已用空间的大小。单位KB。 | -gc -gcold |
PC | 输出持久代空间的大小。单位KB。 | -gc -gccapacity -gcold -gcoldcapacity -gcpermcapacity |
PU | 输出持久代已用空间的大小。单位KB。 | -gc -gcold |
YGC | 新生代空间GC时间发生的次数。 | -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause |
YGCT | 新生代GC处理花费的时间。 | -gc -gcnew -gcutil -gccause |
FGC | full GC发生的次数。 | -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause |
FGCT | full GC操作花费的时间 | -gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause |
GCT | GC操作花费的总时间。 | -gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause |
NGCMN | 新生代最小空间容量,单位KB。 | -gccapacity -gcnewcapacity |
NGCMX | 新生代最大空间容量,单位KB。 | -gccapacity -gcnewcapacity |
NGC | 新生代当前空间容量,单位KB。 | -gccapacity -gcnewcapacity |
OGCMN | 老年代最小空间容量,单位KB。 | -gccapacity -gcoldcapacity |
OGCMX | 老年代最大空间容量,单位KB。 | -gccapacity -gcoldcapacity |
OGC | 老年代当前空间容量制,单位KB。 | -gccapacity -gcoldcapacity |
PGCMN | 持久代最小空间容量,单位KB。 | -gccapacity -gcpermcapacity |
PGCMX | 持久代最大空间容量,单位KB。 | -gccapacity -gcpermcapacity |
PGC | 持久代当前空间容量,单位KB。 | -gccapacity -gcpermcapacity |
PC | 持久代当前空间大小,单位KB | -gccapacity -gcpermcapacity |
PU | 持久代当前已用空间大小,单位KB | -gc -gcold |
LGCC | 最后一次GC发生的原因 | -gccause |
GCC | 当前GC发生的原因 | -gccause |
TT | 老年化阈值。被移动到老年代之前,在新生代空存活的次数。 | -gcnew |
MTT | 最大老年化阈值。被移动到老年代之前,在新生代空存活的次数。 | -gcnew |
DSS | 幸存者区所需空间大小,单位KB。 | -gcnew |
虚拟机常见配置快查
说明 | 命令 |
---|---|
开启 GC Log (java8) | -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:{file-path} |
开启 GC Log (java9) | -Xlog:gc*:file={file-path} |
致谢
1 | java复制代码// 此篇笔记是一个总结分析的笔记 , 时间周期较长 , 很多知识点已经难以追溯出处 , 如果此处遗漏了某位道友 ,敬请谅解 |
更新日志
V20210817 : 改变整体风格
V20210818 : 补充文档
本文转载自: 掘金