总结系列02-java虚拟机JVM

如果文章有错误的话,欢迎指正
内容参考了《深入理解java虚拟机》第四版,想要详细了解的同学可以直接看书

未解决问题

1 读屏障、写屏障、内存屏障
2 SPI机制

1 常用参数设置

参数含义
-UseGCOverheadLimit设置后面的参数关闭垃圾回收异常优化,会报堆内存溢出
-XX:+PrintStringTableStatistics打印字符串常量池的统计信息
-XX:MaxPermsize=8m方法区中持久代(jdk1.6)的最大内存
-Xmx20M堆最大大小20M
-Xms20M堆初始大小20M
-Xmn新生代大小
-XX:InitialSurvivorRatio=8Eden区、from、to 比例为8:1:1
-XX:+PrintGCDetails输出GC详细信息
-XX:+PrintHeapAtGC输出GC前后的堆、方法区可用容量变化
-XX:+PrintGCApplicationConcurrentTime输出GC过程中用户线程并发时间以及停留时间
-XX:+PrintTenuringDistribution输出经历收集之后剩余对象的年龄分布
-XX:PretenureSizeThreshold直接进入老年代的大对象大小
-XX:MaxTenuringThreshold进入老年代的年龄设置
-XX:+HeapDumpOnOutOfMemoryError当JVM发生OOM时,自动生成DUMP文件
-XX:PrintGCApplicationStoppedTime打印stw的时间
-XX:+PrintGCDateStamps输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintReferenceGC打印系统中软引用 弱引用 虚引用
-Dsun.awt.keepWorkingSetOnMinimize=true保证程序在恢复最小化时能够立即响应
-XX:MaxGCPauseMillis=nnn垃圾回收最大停顿毫秒

2 内存结构

在这里插入图片描述
程序计数器(Program counter register)
程序计数器是一块较小的内存空间,它是当前线程执行字节码的行号指示器,字节码解释工作器就是通过改变这个计数器的值来选取下一条需要执行的指令。它是线程私有的内存,也是唯一一个没有OOM异常的区域。

程序计数器的特点
 线程私有
 具有生命周期,随线程启动产生,线程结束消亡
 唯一 一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
 如果线程正在执行的是Java 方法,计数器记录的是正在执行的虚拟机字节码指令地址
 如果正在执行的是Native 方法,则计数器记录值为空(Undefined)

Java虚拟机栈区(Java Virtual Machine Stacks)
也就是通常所说的栈区,它描述的是Java方法执行的内存模型,每个方法被执行的时候都创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法被调用到完成,相当于一个栈帧在虚拟机栈中从入栈到出栈的过程。此区域也是线程私有的内存,可能抛出两种异常:如果线程请求的栈深度大于虚拟机允许的深度将抛出StackOverflowError;如果虚拟机栈可以动态的扩展,扩展到无法动态的申请到足够的内存时会抛出OOM异常。
在这里插入图片描述
本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈发挥的作用非常相似,区别就是虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。
常见的本地方法有hashcode(),wait(),notify()
栈溢出错误:StackOverflowError

堆区(Heap)
所有对象实例和数组都在堆区上分配,堆区是GC主要管理的区域。堆区还可以细分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区(from和to)。此块内存为所有线程共享区域,当堆中没有足够内存完成实例分配时会抛出OOM异常。
OutofMemoryError
在设置对内存大小的时候可以将初始内存大小和最大内存大小设置成一样的,防止内存抖动占用cpu资源

方法区(Method Area)
方法区也是所有线程共享区,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。GC在这个区域很少出现,这个区域内存回收的目标主要是对常量池的回收和类型的卸载,回收的内存比较少,所以也有称这个区域为永久代(Permanent Generation)的。当方法区无法满足内存分配时抛出OOM异常。

运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
在这里插入图片描述
直接内存区(系统内存)
在这里插入图片描述
它并不是虚拟机运行时数据区的一部分,也不是《java虚拟机规范》中定义的内存区域,它也可能导致OOM。

字符串常量池(1.8)
在这里插入图片描述
1.8中字符串常量池放到了堆中
1.6中执行intern方法时,只是将字符串拷贝一份放到常量池中
字符串常量池存放在方法区的永久代中的常量池中

3 堆与栈的区别

1.栈内存存储的是局部变量而堆内存存储的是实体;
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。

4 堆内存运行诊断

**1工具介绍:**
	jps:查看当前系统中有哪些进程
	jmap查看某一个时刻个进程使用堆内存情况
	jconsole 图形界面的多功能的监测工具,可以连续监测
	jvisualvm 可以查找占用堆内存最大的20个对象
**2 使用idea工具分析**
	命令jmap -heap 18756

5 GC的流程

在这里插入图片描述

在这里插入图片描述

7 对象的分配

1对象优先在eden区 分配
对象优先在eden区分配,
eden区没有足够空间时,尝试放入老生代,放不下触发 Minor GC

虚拟机发起 【Minor GC】 = {  
        情况1 : eden 已使用内存 < suvivor内存
            eden 内对象  转移到 suvivor内存
        情况2 : eden 已使用内存 > suvivor内存
            eden 内对象 转移到 老年代 
} 

FULL GC 触发条件 = {
        新生代对象总空间 > 老年代剩余连续空间 
        Minor GC 平均晋升空间大小 > 老年代连续剩余空间,则触发FULL GC 
}

2 大对象进入老年代
避免 剩余较多内存空间 时,直接触发GC

3 长期存活对象进入老年代
对象在Eden出生, 每经历一次minor GC 并被移动到survivor区,则age++
年龄增加到一定程度(15岁),则进入老年代。
此外 当suvivor区中一般以上对象年龄相同,则>=该年龄的对象进入老年代。

8 判断对象死亡 的方法

8.1 引用计数法

在对象中添加一个引用计数器
每当对象被引用,则计数器++;
每当引用失效,则计数器–;
计数器值=0时,标记对象可回收
【存在问题】 : 两个对象相互引用,则无法被回收
【备注】java主流的虚拟机并不是用这个方法

8.2 可达分析算法

(1) 【概念】:
从GC roots对象 向下搜索,如果没有路径到达目标对象。则标记目标对象可回收。

(2) GC roots 对象 = {
1. 虚拟栈中 引用对象
2. 方法区 静态属性的引用对象
3. 常量池引用的对象
4. 本地方方法栈中Native方法引用的对象
5. 所有被同步锁(Synchronized)持有的对象
6. 反应java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存
}
【备注】堆中跨区域引用的对象也可作根对象
【备注】java主流的虚拟机用这个方法

8.3 二次标记

标记可回收的对象,并非立刻回收。他们加入一个队列中进行二次标记。如果在二次标记前,对象产生引用则不会被回收。

9 垃圾收集算法 (GC算法)

垃圾手机算法分为两大类
引用计数式垃圾收集(直接垃圾收集)
追踪式垃圾收集(间接垃圾收集)(重点)

在这里插入图片描述

9.1. 标记-清除算法

标记清除算法过程

(1) 标记需要回收的对象
(2) 统一回收被标记对象
清除的实质是进行覆盖,将被标记的对象的起始位置记录下来,加入到空闲地址列表中,等到存储新内容的时候进行覆盖

标记清除算法优缺点
速度较快
执行效率不稳定
产生【不连续】碎片,内存碎片

9.2 复制算法

新生代分为一块较大的Eden区和两块较小的survivor空间(from和to),每次分配内存只使用Eden和其中一块survivor,发生垃圾回收时,将eden和survivor中的存活对象复制到另一块survivor区中,然后直接将使用过的eden和surivor区全部清除,如果一块survivor区内存不够存储存活的对象,就通过分配担保机制直接进入老年代

缺点:占用双倍的内存空间

9.3 标记-整理算法

过程
(1) 标记要回收的对象
(2) 将存活对象向【一端移动】,清除掉边界外内存。
缺点:因为工作量大,所以速度较慢

9.4. 分代收集算法

新生代内存 采用复制算法
原因: 新生代垃圾回收时,对象死亡率高,则仅复制少数存活对象即可

老年代 采用 标记-清除 || 标记-整理算法
原因: 老年代垃圾回收时,对象死亡率低,仅清除少量死亡对象即可

10 垃圾收集器(垃圾回收机制)

收集算法是方法论,垃圾收集器是具体实现
在这里插入图片描述

10.1 serial 收集器(串行)

新生代收集器
单线程回收器
简单高效
开启方式:-XX:+UseSerialGC=Serial+SerialOld

10.2 ParNew

新生代收集器
它和serial几乎是一样的,是serial的多线程并行版
只能配合CMS工作

10.3 Parallel Scavenge(吞吐量优先)

开启方法:-XX:+UseParallelGC -XX:+UseParalleOldGC

  1. 新生代收集器
  2. 采用复制算法
  3. 注重 吞吐量【cpu效率】
  4. 拥有自适应调节策略
    在这里插入图片描述
    吞吐量:
    执行用户的代码占 的时间/总时间 ,假如前者是99 分钟,后者一分钟,则吞吐量为99%
    停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率的利用处理资源,主要适用于在后台运算而不需要太多交互的分析任务。

10.4 Serial Old

它是Serial 收集器的老年代版
单线程收集
可以与Parallel Scavenge 搭配使用
当CMS收集器发生错误后作为备用收集器

10.5 Parallel Old

吞吐量优先
它是 Parallel Scavenge收集器的老年代版本
支持多线程并发收集
基于标记-整理算法实现

10.6 CMS(响应时间优先)

开启方式:-XX:+UseConcMarkSweepGC,

如果收集失败就会退化为SerialOld回收器
1.【老年代】收集器
2.采用 【标记-清除】算法
3.实现 用户线程 和 GC线程 并发执行

CMS 过程

  1. 初始标记 :STW,标记 GC Root 直接关联的对象,速度很快
  2. 并发标记 :【GC和用户】线程并发,记录所有【可达对象】
  3. 重新标记 : STW,【用户程序】在【并发标记】可能更新【引用域】,所以【暂停用户程序】修正对象【可达性】的变化。
  4. 并发清除 :【GC】线程和【用户程序】并发,GC线程【清除可回收对象】

CMS缺点

  1. 【cpu资源敏感】 : 并发期间,GC程序占用CPU资源,导致用户程序吞吐量低。
    CMS默认启动的回收线程数是(处理器核心数量+3)/4 ,也就是说当处理器数量不足四个 时,CMS对用户程序的影响就可能变的很大。
  2. 【无法处理浮动垃圾】 : 【并发清除】阶段,【GC和用户】程序并发,在该阶段仍会产生垃圾。这些垃圾仅能在下一次GC中回收。有可能出现“Concurrent Mode Failure” 失败进而导致另一次完全的“Stw”的full gc
  3. 由于垃圾回收阶段用户线程还在运行,需要预留足够的内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样待到老年代几乎被填满了在进行收集
  4. 【标记-清除】算法,造成 大量【不连续】空间碎片。碎片过多时会出现老年代剩余空间很多但是没有足够的连续空间分配大的对象,进而触发full gc

10.5 G1收集器

启动方式:-XX:+UseG1GC

设计原则:
1、引入分区的思路,弱化了分代的概念,G1不再坚持固定大小以及固定数量的粉黛区域划分,而是把连续的Java堆划分为多个大小相等的独立区域,每一个region都可以根据需要扮演新生代的Eden空间、Survior空间,或是老年代空间
2、回收时则以分区为单位进行回收。每个分区都可能随G1的运行在不同代之间前后切换。(1MB~32MB, 默认2048个分区)
2、首先收集尽可能多的垃圾(Garbage First),采用启发式收集算法,在老年代找出具有高收集收益的分区进行收集(cms则会在将要耗尽内存时候再回收).
3、G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大。
4、G1的收集都是STW的,采用了混合(mixed)收集的方式,同时收集新生代、老年代,通过限制收集范围来控制停顿时间。

G1优点
1. 并发 :利用多核处理器,既实现用户程序停顿时间短,又一定程度保障CPU吞吐量。
2. 分代收集 : G1收集器【无需其他收集器配合】,独立管理堆。但存在分代收集概念
3. 空间整合 : G1收集器 【整体】采用 【标记-整理】,【局部】(两个region之间)采用 【复制算法】。不产生空间碎片
4. 可预测停顿 G1建立预测模型,预测停顿时长
5. 可根据用户设置停顿时间,制定回收计划(但是也可能存在超出用户的停顿时间).

G1处理流程
1 初始标记(Initial Marking):STW,仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。

2 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。

3 最终标记(Final Marking):STW,对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。

4 筛选回收(Live Data Counting and Evacuation):STW,负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。

适用场景
同时注重吞吐量和低延迟,默认的暂停目标是200ms
超大堆内存,会将堆划分为多个大小相等的region
整体上是标记+整理算法,两个区域之间是复制算法

G1回收的优化
1 jdk8u40字符串去重
2 并发标记类卸载
3 回收巨型对象
4 jdk9并发标记时间的调整,在运行过程中可以动态的调整老年代默认的占比,还添加了一个安全控件,存储浮动的垃圾

10.6 CMS和G1对比

G1 优点:
1 可以指定最大停顿时间
2 分region存储
3 可以按照收益动态确定回收集
4 整体是基于标记-整理,局部标记-复制的清理方法不会生产内存空间碎片,垃圾回收之后仍能提供规整的可用内存,有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存而提前出发下一次收集

G1 缺点:
1 G1为了垃圾收集产生的内存占用和程序运行时的额外执行负载都比CMS要高
内存:虽然他们都使用卡表来处理跨代指针,但是G1的实现更为复杂,因为堆中每个region,无论是扮演新生代还是老年代角色都需要一份卡表,因此G1的记忆集会占用更多的内存空间;
而CMS的卡表只有一份,用来处理老年代到新生代的引用(新生代的对象具有不稳定性,不需要)
在执行负载上:与CMS相比,G1除了需要使用写后屏障来更新维护卡表,还需要使用写前屏障来实现原始快照搜索算法(STAB)

10.7 Shenandoah 收集器

Shenandoah 美 [ˌʃenənˈdoʊə]

这个收集器与G1收集器高度一致,但是存在以下几点不同
1 它支持并发的整理算法
2 它默认不使用分代收集
3 它摒弃记忆集,改用连接矩阵来维护region间的引用关系
4 Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息

工作流程:
1 初始标记(STW),与G1一样,首先标记与GC roots直接关联的对象,停顿时间与堆大小无关,只与root的数量有关

2 并发标记,与G1一样,标记出全部可达对象,时间长短取决于堆中存活对象数量以及对象图的结构复杂程度

3 最终标记(STW),与G1一样,处理剩余的STAB扫描,并在这个阶段统计出回收价值最高的region,将这些region构成一组回收集,会有一小段短暂的停顿

4 并发清理,这个阶段清理那些没有一个存活对象的region

5 并发回收,这个阶段是它与hotspot中其他收集器的核心差异,首先吧回收集中存活的对象复制一份到未使用的region,并且与用户线程并发进行

6 初始引用更新(STW),并发回收结束后需要将堆中所有指向旧对象的引用修正后复制到新地址,但这个阶段并没有实质性操作,只是为了确保并发回收阶段所有的收集器都已完成分配给他们的对象移动任务,会有一段短暂的停顿

7 并发引用更新,真正开始进行引用更新操作,与用户线程并发进行,实际长短取决于内存中涉及引用数量的多少

8 并发清理,到这个阶段整个回收集中所有的region已没有存活对象,最后调用一次并发清理过程来回收这些region的内存空间

10.8 ZGC收集器

他的目标与Shenandoah收集器高度相似,都希望在对吞吐量影响不大的前提下,实现任意堆大小的垃圾收集停顿时间限制在十毫秒以内的低延迟,具有以下特点

1 标记不是在对象上进行而是引入染色指针
2 ZGC也采用基于Region的堆内存布局,但 ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称 为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的 Region可以具有大、中、小三类容量:

·小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。

·中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对 象。

·大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置 4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实 现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到) 的,因为复制一个大对象的代价非常高昂。

执行流程:
1 初始标记,STW
2 并发标记
3 最终标记,STW
4 并发标记预备重分配,根据特定的查询条件统计得出本次收集过程中要清理哪些Region,将这些region组成重分配集
5 并发重分配,这个是核心阶段,把重分配集中的存货对象复制到新的region上,并为重分配集中的每个region维护一个转发表,记录从旧对象到新对象的转向关系。
6 并发重映射,修正整个堆中指向重分配集中对于旧对象的所有引用。由于指针的自愈,重映射并不是很迫切,所以ZGC将重映射的工作合并到了下一次垃圾收集循环中的并发标记阶段里去完成

劣势:
ZGC没有分代的概念,这就限制了它承受的对象分配率不会太高,要想从根本上提升ZGC能够应付的对象分配率,还是需要引入分代收集。

10.9 Epsilon收集器

不会进行垃圾回收的垃圾收集器
书上是这样描述的,如果应用只要运行数分钟甚至数秒,只要JAVA虚拟机能正常分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为的Epsilon便是很恰当的选择

10.9 染色指针

染色指针是最 直接的、最纯粹的,它直接把标记信息记在引用对象的指针上
ZGC的染色指针技术盯上了这剩下的46位指针宽 度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到。Marked1、Marked0表示对象的三色标记状态。当然,由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致 ZGC能够管理的内存不可以超过4TB

优势:
1 染色指针式的一旦某个Region的存货对象被移走之后,这个region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该region的引用都被修正之后才能清理
2 染色指针可以大幅减少在垃圾回收过程中内存屏障的使用数量,因为直接将对象的引用变动记录在了指针中

10.6 STW

用户线程暂停必须在安全点:方法调用、循环跳转、异常跳转

抢先式中断(Preemptive Suspension)

抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。

主动式中断(Voluntary Suspension)

主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区域
指在一段代码片段中,引用关系不会发生变化。在这个区域中任意地方开始GC都是安全的。也可以把Safe Region看作是被扩展了的Safepoint。

10.7 记忆集

在收集垃圾时遇到跨代引用时,避免为了少量的跨代引用去扫描整个老年代,引入了记忆集。
用于记录从非收集区域指向收集区域的指针集合的抽象数据结构

10.8 并发的可达性分析

可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析,这意味着必须全程冻结用户线程的运行。并发的执行可达性标记可能会导致并发扫描时对象消失,需要使用增量更新或原始快照来解决。

11 内存分配与回收策略

以Serial收集器分析

1对象优先在Eden分配
大多数情况对象先分配在新生代Eden区,当Eden中没有足够空间时,会触发Minor GC
当Eden区的存活对象内存大于survivor空间内存时,通过分配担保机制提前转移到老年代
2 大对象直接进入老年代
为了避免大对象在eden区和survivor区来回复制,可以使用参数-XX:PretenureSizeThreshold配置大于该值的对象直接进入老年代
3 长期存活的对象
每经历一次minor GC,新生代中的对象年龄就会+1,可以通过参数- XX:MaxTenuringThreshold=15 配置当对象年龄到达多少的时候进入老年代,默认15
4 动态对象年龄判断
如果在Survivor空间中相同年龄所有对象大小的总和大于整个Survivor空间的1/4,大于或等于该年龄的对象就可以直接进入老年代

5 空间分配担保
jdk 6 Update24 之后 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行minor GC,否则将进行full GC

12 哪些内存需要回收

jvm对不可用的对象进行回收,哪些对象是可用的,哪些是不可用的?Java并不是采用引用计数算法来判定对象是否可用,而是采用根搜索算法(GC Root Tracing),当一个对象到GC Roots没有任何引用相连接,用图论的来说就是从GC Roots到这个对象不可达,则证明此对象是不可用的,说明此对象可以被GC。对于这些不可达对象,也不是一下子就被GC,而是至少要经历两次标记过程:如果对象在进行根搜索算法后发现没有与GC Roots相连接的引用链,那它将会第一次标记并且进行一次筛选,筛选条件是此对象有没有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用执行过一次,这两种情况都被视为没有必要执行finalize()方法,对于没有必要执行finalize()方法的将会被GC,对于有必要有必要执行的,对象在finalize()方法中可能会自救,也就是重新与引用链上的任何一个对象建立关联即可。

16 多线程对象的分配过程

16.1依据逃逸分析,判断是否能栈上分配?

如果可以,使用标量替换方式,把对象分配到VM Stack中。如果 线程销毁或方法调用结束后,自动销毁,不需要 GC 回收器 介入。
  否则,继续下一步。

16.2 判断是否大对象?

如果是,直接分配到堆上 Old Generation 老年代上。如果对象变为垃圾后,由老年代GC 收集器(比如 Parallel Old, CMS, G1)回收。
  否则,继续下一步。

16. 3 判断是否可以在 TLAB中分配?

如果是,在 TLAB中分配堆上Eden区。
否则,在 TLAB外堆上的Eden区分配。

逃逸:

如果一个在方法内定义对象在方法外被引用那么垃圾回收时就不能回收该变量,叫做逃逸

标量:

基础类型和对象的引用

聚合量:

比如对象,它可以进一步分解为各种标量

标量替换:

标量替换就是,将聚合量分解为分散的变量,只在粘中或者寄存器中创建使用到的成员标量

大对象到底多大:

XX:PreTenureSizeThreshold=n (仅适用于 DefNew / ParNew新生代垃圾回收器 ) https://bugs.openjdk.java.net/browse/JDK-8050209
  G1回收器的大对象判断,则依据Region的大小(-XX:G1HeapRegionSize)来判断,如果对象大于Region50%以上,就判断为大对象Humongous Object。

TLAB:

全称Thread Local Allocation Buffer, 即:线程本地分配缓存。这是一块线程专用的内存分配区域。
  TLAB占用的是eden区的空间。
  在TLAB启用的情况下(默认开启),JVM会为每一个线程分配一块TLAB区域。

为什么需要TLAB?

这是为了加速对象的分配。
  由于对象一般分配在堆上,而堆是线程共用的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降。
  考虑到对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率。
  局限性: TLAB空间一般不会太大(占用eden区),所以大对象无法进行TLAB分配,只能直接分配到堆 Heap上。

17 一个线上的程序如果CPU飙高怎么解决

先将线程列出来,查看是业务线程还是垃圾回收线程导致的
使用 jmap -histo 1196 | head -20来查看占用内存较多的对象

18 GC调优

在这里插入图片描述

18.1新生代调优

各区域大小设置
新生代内存设置>堆内存的百分之25,小于堆内存的百分之五十
最理想的情况是
在这里插入图片描述
在这里插入图片描述
晋升阈值的设置
展示幸存区各对象调用次数以及大小信息
在这里插入图片描述

18.2老年代调优

设置当老年代的比率达到多少时进行垃圾回收
在这里插入图片描述
在这里插入图片描述
案例1 .解决方法:增大新生代内存;增大晋升阈值
案例2:查看GC日志,一般重新标记占用时间较长
在这里插入图片描述
设置再重新标记之前,先对新生代的对象做一次垃圾清理
案例3:1.7采用永久代进行方法区的实现,1.8之后使用元空间,永久代空间不足也会造成full Gc

19 运行期优化

19.1分层编译

在这里插入图片描述

19.2 逃逸分析

逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。

逃逸状态
1、全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
对象是一个静态变量
对象是一个已经发生逃逸的对象
对象作为当前方法的返回值
2、参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
3、没有逃逸
即方法中的对象没有发生逃逸。

逃逸分析优化

  1. 锁消除
    我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
    例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。
  2. 标量替换
    首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。
    对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
    这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。
  3. 栈上分配
    当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。

20 finalize关键字

当一个对象在堆内存中运行时,根据它被引用变量所引起的状态把他们分为以下三种状态
1:可达状态:有一个以上的引用变量引用它
2:可恢复状态:某个对象不再有任何变量引用它,进入可恢复状态。
这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收对象之前,系统调用所有可恢复状态的对象的finalize()方法,进行资源清理,如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态,否则,该对象进入不可达状态
3:不可达状态:对象与所有引用变量的关联都被切断,且系统已经调用过所有对象的finalize()方法,仍然没有使该对象变为可达状态,那么该对象将永久的失去引用,变为不可达状态,系统才会真正的回收该对象所占用的资源

finalize()方法具有如下4个特点:
(1)永远不要主动调用某个对象的finalize()方法,该方法应该交给垃圾回收机制调用;
(2)finalize()方法何时被调用、是否调用具有不确定性,finalize()方法不一定会执行;
(3)JVM执行可恢复对象的finalize()方法时,可能使该对象或其他对象变为可达状态;
(4)JVM执行finalize()方法出现异常时,垃圾回收机制不会报告异常,程序继续执行。

24 调优案例分析

24.1 大内存硬件上的程序部署策略
当前单体应用在大内存硬件上主要有两种部署方式:
1)通过一个单独的java虚拟机实例来管理大量的java内存
2)同时使用若干个java虚拟机,建立逻辑集群来利用硬件资源
背景:

低访问量的文档网站
启动了一个虚拟机
垃圾收集器:吞吐量优先的收集器
堆内存大小固定在12G
该服务器,没有部署其他应用
问题:隔几分钟就会出现十几秒的停顿
原因:对过大的堆内存进行回收时会带来长时间的停顿
解决方式:控制Full GC的频率
分析:
控制Full GC的关键时老年代的相对稳定,这主要取决于应用中绝大多数对象能否符合“朝生夕灭”的原则,即大多数对象的生存时间不应太长,尤其不能有成批量的、长时间生存的大对象产生。

解决方案:
建立5个32位JDK的逻辑集群,在建立一个Apache服务作为前端均衡代理
将垃圾收集器更换为Cms

24.2 不恰当数据结构道指内存占用过大
背景:
一个后台RPC服务器,使用64位java虚拟机,内存配置位,-Xms4g -Xmx8g, -Xmn1g
垃圾收集器为ParNew+CMS

问题描述:
平时的minor GC时间在30毫秒以内,但业务每隔十分钟需要加载一个约80M的数据文件进行分析,这样会在内存中形成超过100万个HashMap<Long,Long> entry,导致Minor Gc的时间超过500ms

分析:
文件分析期间,Eden空间很快被填满引发Minor Gc,但是垃圾回收之后大部分的对象仍然存活,而ParNew使用的是复制清除法,如果对象存活过多,复制这些对象,并且维护对象引用的正确性就会导致垃圾收集的暂停时间变长

解决方式:
1 仅从GC调优的角度考虑,可以直接将survivor空间去掉,让新生代存活的对象直接进入老年代,但这种方式治标不治本
2 修改代码,使用HashMap<Long,Long> 结构来存储数据文件空间效率太低

25类文件结构

25.1 无关性的基石

各种不同平台的java虚拟机,以及所有平台都统一支持的程序存储格式–字节码是构成平台无关性的基石。实现语言无关性的基础仍然是虚拟机和字节码存储格式。
java虚拟机不与包括Java语言在内的任何程序语言绑定,他只与“class文件”这种特定的二进制文件格式所关联,class文件中包含了java虚拟机指令集、符号表以及若干其他辅助信息

25.2 class文件的结构

Class文件采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有“无符号数”和“表”两种数据类型
无符号数: 属于基本的数据类型,以U1、U2。。。来分别代表一个字节、2个字节。。。
无符号数可以用来描述数字、索引引用、数量值或者按照UTF8编码构成字符串值
表:由多个无符号数或者其他代表作为数据项构成的复合数据类型

25.3 常量池

常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic reference),主要包括下面几类常量:
被模块导出或者开放的包
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
方法的句柄和方法类型
动态调用点和动态常量

在class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期间转换的话是无法得到真正的内存入口地址,也就是无法直接被虚拟机使用。
当虚拟机做类加载时,将会从常量池得到对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存之中。

25.4 字节码指令

java虚拟机的指令由一个字节长度的操作码(opcode)和零至多个操作数(Operand)构成

java虚拟机的解释器执行模型:
do{
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while(字节码流长度>0);p

26 虚拟机类加载机制

虚拟机的类加载机制:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型
在这里插入图片描述

26.1 类的加载时机

加载、验证、准备、初始化和卸载这五个阶段是按部就班的开始,解析阶段不一定,如下图类的生命周期
在这里插入图片描述

有且只有六种情况必须对类进行初始化(加载、验证、准备需要在此之前开始):
1)遇到new、getstatic、putstatic、invokestatic这四条指令时,如果类型没有进行过初始化,则需先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
使用new关键字实例化对象的时候
读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
调用一个类型的静态方法的时候
2)使用java.long.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
3)当虚拟机启动时,用户需要指定一个要执行的主类(包含main() 方法的那个类),虚拟机会先初始化这个主类
5)当使用jdk7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_geStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
6)当一个接口中定义了JDK8新加入的默认方法(被default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

注:接口与类的初始化场景仅第三种情况不同,当一个接口初始化时,并不要求其父接口全部都完成了初始化,只有在真正用到父接口的时候(如引用接口中定义的常量)才会初始化

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

验证
这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全,主要包括以下四个阶段:

1)文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
只有通过了这一阶段之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储
2)元数据验证
第二阶段是对字节码的信息进行语义分析,以保证其描述的信息符合《Java语言规范》
3)字节码验证
这一阶段的主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
4)符号引用验证
最后一个校验行为发生在虚拟机将符号引用转化为直接引用的时候,他可以看做是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验

准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,注意,

注意:
1 在JDK8之后类变量会随着CLass对象一起存放在Java堆中,这时“类变量在方法区”就完全是一种对逻辑概念的表达了。
2 准备阶段进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中,其次这里说的初始值通常情况下是数据类型的零值,真正的赋值要到初始化阶段才会被执行。如果是被final修饰的类变量将会在准备阶段直接赋值
在这里插入图片描述

解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。虚拟机并未规定解析阶段的具体时间

符号引用:用一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可
直接引用:指可以直接指向目标的指针、相对便宜量或是一个能间接定位到目标的句柄

初始化
初始化阶段是类加载过程的最后一个步骤,在这个阶段,Java虚拟机才真正开始执行类中编写的程序代码,将主权移交给应用程序,这个阶段就是执行方法的过程,他是由编译器自动收集类中所有的类变量(静态变量)的赋值动作和静态代码块中的语句合并产生的;

1.是线程安全的,在多线程环境中被正确地加锁、同步
2.对于类或接口来说是非必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 clinit
3.接口与类不同的是,执行接口的 clinit不需要先执行父接口的 clinit,只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的clinit

22 三层类加载器

在这里插入图片描述【种类】
(1) 【BootStrap】启动 类加载器
(2) 【Extension】扩展 类加载器
(3) 【Application】应用程序 类加载器
(4) 【User】自定义 类加载器

注:
1.类加载器Java类 同普通Java类一样,也需要通过 类加载器加载。
2. 一个类只能加载一次
3.判断两个class是否相同的时候同样会判断class的类加载器是否相同

23 类加载器的双亲委派模型

工作过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器去完成,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围内没有找到所需的类),子加载器才会尝试自己去加载

注意:不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式

优点:
1类会随着它的类加载器一起具备带有优先级的层次关系,可保证Java程序的稳定运作;
2实现简单,所有实现代码都集中在java.lang.ClassLoader的loadClass()中
3避免类的重复加载
4类加载器 加载 自己管理【范围】内的类

注意:jdk有时会打破双亲加载机制,比如使用jdbc时,即使不运行语句Class.forName(“com.mysql.jdbc.Driver”);
也可以正常的连接,是因为下面的DriverManager类中定义了一个静态代码块来加载驱动类
这个时候就破坏了双亲委派机制,并不使用启动类去加载,而是使用的应用程序加载类

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值