java计数器占位符_doc/JVM.md · 有两把刷子的程序猿/jvm - Gitee.com

# JVM & gc

## 基础知识

### java命令参数

#### VM options

VM options其实就是我们在程序中需要的运行时环境变量,它需要以-D或-X或-XX开头,每个参数使用空格分隔

使用最多的就是-Dkey=value设定系统属性值,比如-Dspring.profiles.active=dev3

如果是布尔值参数,-XX:+args表示增加参数,-XX:-args表示去掉参数

#### Program arguments

Program arguments为我们传入main方法的字符串数组args[],它通常以--开头,如--spring.profiles.active=dev3

等价于-Dspring.profiles.active=dev3如果同时存在,以Program arguments配置优先

#### Environment variables

Environment variables没有前缀,优先级低于VM options,即如果VM options有一个变量和Environment variable中的变量的key相同,则以VM options中为准。

![java命令参数](java参数.png)

#### finaliza机制

当对象被销毁前会调用对象的finaliza方法,执行一些回收操作,比如关闭文件、关闭连接等

> 需要特别注意的是,当对象在软引用可达时,如果对象被回收执行finalize方法,通过对象重新指定引用,可复活此对象,如果复活后再次被gc回收,将不会再次执行finaliza方法,即finalize方法只执行一次

一般不建议采用finaliza方法释放资源,理由有三点:

1. finalize可能导致对象复活

2. 执行时间没有保障,如果没有gc,则finalize方法永远不会执行

3. 一个糟糕的finalize会严重影响gc性能

```java

public class MyObject {

String name;

public MyObject(String name) {

this.name = name;

}

/**

* 被回收时执行回调此方法

* @throws Throwable

*/

@Override

protected void finalize() throws Throwable {

System.out.println("MyObject " + name + " gc...");

}

}

```

### Java引用

引用强度:强引用 > 软引用 > 弱引用 > 虚引用

#### 强引用

直接通过new出来赋值的引用,比如 Object o = new Object(),o就是强引用,被强引用引用的对象不会被gc回收

#### 软引用(SoftReference)

被软引用的对应在jvm内存不足时才会被回收,所以不能保证gc后软引用的对象一定会被回收。

> 被gc回收会,软引用会被加入到回收队列里面,而引用的对象已经被回收

```java

ReferenceQueue queue = new ReferenceQueue<>();

SoftReference reference = new SoftReference(new MyObject("1"), queue);

System.out.println("引用值:" + reference.get()); //not null

System.out.println("是否加入已回收队列:" + reference.isEnqueued()); //false

System.gc();

Thread.sleep(1000); //延时1秒,等待gc完毕

System.out.println("gc后,引用值:" + reference.get()); //not null,因为内存不足才回收

System.out.println("gc后,是否加入已回收队列:" + reference.isEnqueued()); //false

```

#### 弱引用(WeakReference)

如果对象只被弱引用引用,则在gc时会回收此对象

> 被gc回收会,弱引用会被加入到回收队列里面,而引用的对象已经被回收

```java

ReferenceQueue queue = new ReferenceQueue();

WeakReference reference = new WeakReference(new MyObject("1"), queue);

System.out.println("gc前,引用值:" + reference.get()); //not null

System.out.println("gc前,是否加入已回收队列:" + reference.isEnqueued()); //false

System.gc();

Thread.sleep(1000); //延时1秒,等待gc完毕

System.out.println("gc后,引用值:" + reference.get()); //null

System.out.println("gc后,是否加入已回收队列:" + reference.isEnqueued());//true

Reference ref = null;

while((ref = queue.poll()) != null) {

System.out.println("gc后,是否找到回收引用:" + (ref == reference)); //true

}

```

#### 虚引用(PhantomReference)

虚引用是最弱的弱音,创建虚引用时必须指定回收队列

> 执行gc后,虚引用才会被放入回收队列,而get方法是虚引用创建后就一直返回null

```java

ReferenceQueue queue = new ReferenceQueue<>();

//MyObject obj = new MyObject("1");

String obj = new String("23123123");

PhantomReference reference = new PhantomReference<>(obj, queue);

System.out.println("引用值:" + reference.get()); //null

System.out.println(reference.isEnqueued()); //false

System.out.println(queue.poll() == reference); //false

System.gc();

Thread.sleep(1000); //延时1秒,等待gc完毕

System.out.println(reference.isEnqueued()); //true

System.out.println(queue.poll() == reference); //true

```

### 字符串常量池

常量池是一种节约内存的方式。java7之前,字符串放在Perm永久代中,后面放在了Heap堆里面。

要将字符串放进常量池有两种方法:

1. 直接用双引用定义变量的方式

2. 调用字符串的intern()方法

```java

String s = new String("1"); //新创建一个字符串"1",但没有放入常量池

s.intern(); //放入常量池,并返回常量池地址,java7之前返回的是Perm代地址,java7之后返回的是heap地址

String s2 = "1"; //会尝试放入常量池中,虽然常量池已经有了此对象,但是返回的是堆上的地址

System.out.println(s == s2); //false

System.out.println(s.intern() == s2); //true

//java7之前相当于String s5 = new String("3");s5.intern();

//java7之后相当于String s5 = new String("3").intern();

String s5 = "3";

String s6 = "3";

System.out.println(s5 == s6); //java7之前为false,java7之后为true

```

### Java对象头

在HotSpot虚拟机中,java对象包含三部分:对象头、实例数据、对齐填充

* 对象头:包含gc分代年龄、hash码、锁标志、类型指针,指向对象的class元数据

* 实例数据:函数各个属性的数据,包括父类继承而来的属性

* 对齐填充:占位符,不一定存在

### JIT(Just-In-Time)即时编译技术

java同时支持解释执行(interpreter又称转义执行)和编译执行(compile,JIT技术)模式。解释执行是把java字节码解释一条成本地机器码然后执行一条,而编译执行即把java字节编译成本地机器码并保存下来,以后直接用机器码执行,不用再次解释成本地机器码,所以编译执行的效率比解释执行高很多,C/C++采用静态编译技术直接把源码编译成机器码,而java采用JIT即时编译技术在运行时把java字节码编译成机器码。但是如果jvm就直接全部把字节码编译成机器码会造成启动缓慢,所以java会先采用解释执行,然后对于热点代码采用JIT即时编译并缓存起来,所以混合模式下,JVM会自动判断采用解释执行还是编译执行

![JIT(Just-In-Time)即时编译技术](jit.png)

```bash

java -Xcomp -version #编译模式执行

java -Xint -version #解释模式执行

java -Xmixed -version #混合模式执行,默认也是混合模式

```

### GC根对象

根对象是用来跟踪heap堆中哪些对象被引用,没有被引用的对象是gc想要清除的对象

判断对象是否存活的方式一般两种:

1. 引用计数,新增一个引用就+1,释放一个引用就-1,方法简单但是无法解决循环依赖的问题

2. 可达性分析:从根对象遍历对象链,如果对象不在任何一条对象链中说明没有引用指向他

可以成为gc根的对象:

1. 栈中引用的对象

2. 本地方法栈JNI引用的对象

3. 静态变量和静态常量引用的对象

> 静态变量和静态常量的区别,访问静态变量会触发class加载,执行static代码块,而访问静态常量则不会

### GC算法

#### 标记-清除(Mark-Sweep)

标记阶段遍历内存空间标记出没有被引用的对象,清除阶段把没有被引用的对象清除,这样会造成内存碎片

#### 复制(Copy)

把内存空间分为相同大小两份,GC时把有引用的对象复制到另一边,这样会浪费一半的内存空间

> java年轻代Eden复制到Survivor、Survivor From复制到Survivor To、年轻代复制到年老代,都是复制算法

#### 标记-整理(Mark-Compact)

标记-整理结合了标记清除和复制的优点,标记阶段遍历内存空间标记出没有被引用的对象,整理阶段把没有被引用的清楚,并把存活下来的对象移动到一起

> 年老代的gc算法

#### 分代收集(Generational Collection)

这才是java采用的垃圾收集算法,综合了上述算法的优点

> j2se 1.2后开始使用

### jvm分代模型

![jvm分代](jvm分代.png)

* jvm分代:年轻代(Young)、年老代(Old/Tenured)、永久代(Perm),

* 其中年轻代又分为Eden区和Survivor区,默认比例8:2,Survivor区又分为From和To区,各占1/10

* 发生在年轻代的gc叫做minor gc,发生在年老代gc叫做major gc或者full gc,full gc发生的概率一般远远小于minor gc

### 对象提升(Promotion)规则

![jvm对象提升](jvm对象提升.png)

1. 进入Eden区:对象优先分配在Eden区,如果没有足够的空间,则执行一次minor gc,如果还是不够直接分配在老年代

2. 进入Survivor区:对象经过minor gc后,被复制到Survivor区,每经过一次gc,对象年龄计数器+1

3. Survivor From和To间复制:

4. 进去老年区:当对象年龄达到-XX:MaxTenuringThreshold值进入老年代;或者每次复制到年老代的平均大小大于老年区剩余空间

5. 老年代full gc后还是装不下,报OutOfMemory

## 深入知识

### JVM内存模型

![jvm对象提升](jvm结构.png)

java虚拟机将内存数据分为程序计数器、栈、本地方法栈、堆、方法区五个部分

* 程序计数器:存放下一条指令的地址

* 栈和本地方法栈:存放函数调用堆栈信息

* 堆:存放java运行时公用的对象数据

* 方法区:存放类元数据信息

### 程序计数器

* 属于线程私有空间

* 是唯一没有在java虚拟机规范中规定OutOfMemoryError情况的地方。

> 用寄存器实现,速度比内存更快

### 栈

![jvm栈帧](jvm栈帧.png)

* 用栈和栈帧结构保存方法调用关系

* 属于线程私有空间

* 两种异常:StackOverflowError(超出最大栈深度)、OutOfMemoryError(超出-Xss配置)

* -Xss1m:设置栈的最大内存空间

* 由于java采用预先分配内存的方式,栈必须预先知道所用空间的大概大小,所以java的实现要把对象放在堆中,只把一些基础类型数据放在栈中。

* 栈帧:局部变量表、操作数栈、栈帧数据区(常量池引用、方法返回地址、异常表)

* 在方法内对应一个大变量如果不用了,可以先置为null,方便gc回收,否则如果方法后面的语句需要花很久,可能导致大变量不能及时回收

> 用内存实现,使用JClassLib可以查询每个方法分配最大局部变量空间大小,他还可以查询class各种信息

### 本地方法栈

用C实现,他们内部结构和jvm类似,但是他们可以操控jvm的内存和寄存器,所以他和jvm的权限一样大

### 堆

* 多线程共享

* 通过new创建的对象都会保存在堆中,不用显示回收对象,因为jvm会自动帮我们回收

* 堆的大小是可变的,所以堆只是逻辑上连续的,物理上可以不是连续的

* new对象时,需要在堆上动态分配,不像栈采用预先分配的分时,所以堆的速度比栈慢

* java7开始支持逃逸分析和栈上分配:在方法中,如果没有逃逸的对象,直接在栈上分配空间,减少gc压力。三种逃逸方式:变量赋值给全局变量、变量被return、变量被当做参数传给其他方法

> 逃逸分析还可以用来取消同步语义

> 不推荐使用jmap -heap和-histo命令,可能造成jvm进程僵死,可使用jstat -gc和-gccapcity

**参数设置:**

* -Xmx1G -Xms1G:设置堆的最大1G,初始大小1G

* -Xmn1G:设置年轻代为1G,等同于设置-XX:NewSize=1G和-XX:MaxNewSize=1G

* -XX:NewRatio=4:为年老代和年轻代的比例,年轻代所占比例=1/(1+4)=1/5

* -XX:SurvivorRatio=4:为eden和一个survivor大小的比例,所以eden所占大小比例=4/(1+1+4)=2/3

* -XX:MaxTenuringThreshold=6:设置晋升到老年代的年龄大小

* -XX:PermSize=1G、-XX:MaxPermSize=16m:设置永久代的初始大小和永久代的最大大小

### 方法区(HotSpot中也叫永久代,PermGen Space)

java8之后之后没有永久代

![jvm方法区](jvm方法区.png)

* 主要存储类的元信息(java8移动到metaSpace)、字符串常量池(java7后迁移到堆)、静态属性(java7后迁移到堆),jvm本身一些数据

* 多线程共享

* 只有在full gc期间才被回收,一回收类元信息,二回收常量池

* -XX:PermSize=1m -XX:MaxPermSize=1m

### 可以触发full gc情况

* System.gc()(-XX:+DisableExplicitGC来禁止RMI调用)

* 老年代空间不足(gc后空间仍不足,抛出java.lang.OutOfMemeoryError:java heap space)

* 统计每次晋升到老年代的平均大小大于老年代剩余空间

* 永久代空间满了(gc后空间仍不足,抛出java.lang.OutOfMemeoryError:PermGen space)

### STW(Stop The World)

gc时会有短暂的停止所有用户线程

### 堆外内存

线程栈、NIO缓冲都是堆外内存,使用堆外内存和对象池能减少STW时间,生命周期长但重复的对象的适合用对象池,生命周期短的适合gc,生命周期长的适合堆外内存

堆外内存有点:

1. 可以扩展至很大的空间,1TB甚至比内存大

2. 减少gc暂停时间

3. 进程间共享,减少复制

### MarkOop

是对象的一个header字段,用来表示是当前对象的状态

### 垃圾收集器分类

1. 按线程分类:串行收集器(Serial)、并行收集器(ParNew、parallel、cms、g1)

2. 按工作模式分类:并发式(与应用线程交替进行)、独占式(gc时,STW停止应用线程)

3. 按碎片处理方式:压缩式、非压缩式

4. 按分代分类:年轻代收集器、年老代收集器

#### serial收集器

* 年轻代兼年老代收集器:年轻代使用复制算法(适用于单核并且内存不大的机器),年老代使用标记-压缩算法

* 是jvm client模式默认收集器

* java的第一代收集器,串行独占式收集器

* -XX:+UseSerialGC年轻代和年老代同时开启串行收集器

* 日志标志DefNew

#### ParNew收集器

* 年轻代收集器

* 是serial年轻代收集器的多线程并行版,和serial几乎一样,只是采用了多线程

* 使用-XX:+UserParNewGC,年轻代开启并行收集器,年老代默认串行收集器

* 日志标志ParNew

#### Parallel收集器

* java7/java8默认收集器

* 是Serial收集器的并行版本,支持年轻代、年老代

* 日志标志PSYoungGen

* 和ParNew不同的是,他是吞吐量优先的收集器

* 可以通过设置一个大于零的数-XX:GCTimeRatio=99控制gc时间占比,1/(1+n)=1/100,

* -XX:MaxGCPauseMills设置最大gc时间,这两个参数互斥的,即吞吐量和低延迟是互斥的。

* -XX:UseAdaptiveSizePolicy打开自适应策略,这种设置下年轻代大小、Eden和Survivor比率、晋升老年代年龄等都自动调整。

* -XX:ParallelGCThreads设置线程数量

* 使用-XX:UseParallelGC年轻代并行收集器,年代老默认串行收集器

* 使用-XX:UseParallelOldGC则年轻代和年老代同时使用并行收集器

#### CMS收集器(Concurrent-Mark-Sweep)

* 面向场景高并发、低延迟(这点和ParallelOld刚好相反)

* 采用标记-清除算法

* 只用于年老代

* 日志标志garbage-first heap

* 使用-XX:UseConcMarkSweepGC表示年老代用CMS收集器,年轻代用ParNew收集器

**CMS四个阶段:**

* 初始标记(会STW):暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快

* 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

* 再次标记(会STW):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

* 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

![CMS收集器](CMS收集器.png)

**缺点:**

1. 采用标记-清除算法会造成很多碎片,为了清理碎片还是会开启full gc整理内存

2. 并发标记阶段无法检测到新的垃圾对象

**相关参数说明:**

* -XX:UseCMS-CompactAtFullCollection指定执行full gc对内存进行整理

* -XX:CMSFullGCs-BeforeCompaction指定多少次full gc后对内存进行整理

* -XX:CMSInitiatingOccupancyFraction指定年老代使用内存占比达到多少开始cms回收,设置高可以有效减少full gc次数

### G1收集器

![](G1收集器.png)

* 年轻代搜集器是并行独占式

* java9之后默认收集器

* G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字 Garbage-First 的由来)

**特点:**

* 并行性:回收期间,可以利用多个gc线程并行收集,充分多核的计算能力

* 并发性:g1回收期间可以应用线程一起并发交替工作,部分阶段还可以和应用线程并行工作

* 分代性:g1和其他垃圾回收器不一样,g1同时兼顾了年轻代和老年代,比如CMS只能用于老年代

* 空间性:CMS回收垃圾只会做简单的清除工作,而g1会把可用对象进行复制和移动,不会造成内存碎片

* 可预见性:g1可以只选取部分region进行垃圾清除,因此对gc时间可控

**相关参数:**

* -XX:ConcGCThreads 并发线程数

* -XX:G1HeapRegionSize 设置region大小

* -XX:G1HeapWastePercent 默认5%,表示允许5%以下垃圾不用清理

* -XX:G1MixedGCCountTarget 默认8,设置一次并行循环后启动多少个混合GC

* -XX:+G1PrintRegionLivenessInfo 打印标记循环阶段每个region详细的诊断信息,需开启-XX:UnlockDiagnosticVMOptions并放在前面

* -XX:G1ReservePercent 默认10%,设置保留不能用于年轻代的内存

* -XX:+G1PrintRSetStats 打印RSet诊断信息,需开启-XX:UnlockDiagnosticVMOptions并放在前面

* -XX:+HeapDumpBeforeFullGC/-XX:+HeapDumpAfterFullGC 堆内存信息备份,用于分析gc

* -XX:InitiatingHeapOccupancyPercent 老年代大小/heap大于这个百分比,则启动并行循环垃圾回收

* -XX:+UseStringDeduplication/-XX:StringDeduplicationAgeThreshold/-XX:+PrintStringDeduplicationStatistics g1特性选项,当对象年龄达到临界值或者在老年代就能对象去重

* -XX:MaxPauseMills 设置g1的目标停顿时间,默认200ms

* -XX:MinHeapFreeRatio 设置堆的最小空闲比例,如果比这个还小就扩展,默认40%

* -XX:MaxHeapFreeRatio 设置堆的最大空闲比例,如果比这个还大就缩小,默认70%

* -XX:+PrintAdaptiveSizePolicy 开启堆内存大小变化打印相应日志信息

* -XX:+ResizePLAB 本地线程的本地缓存空间是否可动态调整大小,默认开启(最好关闭,因为可能增加gc停顿时间)

* -XX:+ResizeTLAB 应用线程的本地缓存空间是否可动态调整大小,默认开启

* -XX:+ClassUnloadingWithConcurrentMark 是否在gc并行循环期间卸载class,默认开启

* -XX:+ClassUnloading 是否卸载无用的类,无论循环gc还是full gc,默认true

* -XX:+UnlockDiagnosticVMOptions 是否开启诊断选项,默认false

* -XX:+PrintFlagsFinal 打印所有参数值

* -XX:+UnlockExperimentalVMOptions 是否开启试验选项,默认false,比如配合:-XX:G1NewSizePercent(默认5%)

* -XX:+UnlockCommercialFeatures 是否解锁商业特性

#### 元空间(metaspace)

* java8后metaspace存储类的元信息,不再有OutOfMemory:PermGen异常

* 使用本地内存存储

**相关参数:**

* -XX:MetaspaceSize:初始内存大小,建议初始设大一点,避免不足引发full gc

* -XX:MaxMetaspaceSize:设置最大占用内存(默认全部内存)

* -XX:MinMetaspaceFreeRatio:扩大比例设置,当gc后可用空间占比小于这个设置就扩大

* -XX:MaxMetaspaceFreeRatio:缩小比例设置,当gc后可用空间占比大于这个设置就缩小

#### 混合事件(Mixed GC Event)

所有的年轻代Region和一部分年老代一起被回收,混合事件一定在Minor gc之后

#### 回收触发条件(Reclaimable)

有一条可回收链表,里面链接的都是可以回收的Region,即里面的Region都是对象存活率小于-XX:G1MixedGCLiveThresholdPercent(默认85%),如果整条链的Region总大小超过整个堆的-XX:G1HeapWastePercent(默认5%)便启动回收机制

#### RSet(Remember Set)

* 每个Region都有会一个RSet,用来储存其他地方指向这个Region的引用

* RSet占总jvm内存小于5%

* RSet可以让堆内存使用率很高在触发full gc,同时minor gc的STW时间更加可控

**参数:**

* -XX:G1RSetUpdatingPauseTimePercent设置更新RSet占STW的时间占比,默认10%

* -XX:G1ConcRefinementThreads设置更新RSet的线程数量

#### CSet(Collection Set)

* 即收集集合,保存一次gc中需要回收的Region

* CSet中所有存活下来的数据都会被复制移动

* CSet占用JVM空间的总大小不超过1%

#### G1 Pause Time Target

表示g1垃圾回收的停顿时间,g1收集阶段是独占式的,设置预期停顿时间,g1会根据停顿时间控制回收的Region数量

#### TLAB(Thread Local Allocation Buffers)

即线程专用的内存分配区域

#### 分区(Region)

* g1把jvm对划分为多个region,每个region属于某个分代,有:Eden、Survivor、Old,空的region不属于任务分代

* 每个Region大小相同并且为2的整数倍,范围在1M ~ 32M之间

* 最多2000个region

#### 大对象分区(Humongous Region)

* 大对象是指一个对象大小超过region的50%大小

* 大对象Region属于老年代的一部分

* 当分配一个对象的大小超过一个region的大小,需求多个连续相连的region装配

* 频繁的分配年龄短大对象,会造成性能问题,因为因为大对象会和年轻代一起回收

#### 全垃圾回收(Full Garbage Collection)

G1的Full gc采用和Serial gc同一种算法,full gc会对整个堆进行回收并且是单线程,设计目标是为了减少Full GC的发生次数,会造成很长的停顿

#### 并行循环

**g1并行循环阶段:**

1. 初始标记

2. 并行Root区间扫描

3. 并行标记

4. 重标记

5. 清除

#### 堆大小

1. g1的堆大小也是由-Xms,-Xmx配置

**可能增加堆大小的情况:**

1. full gc

2. gc花费时间达到配置-XX:GCTimeRatio配置,g1默认9,其他gc默认99

3. 一个对象分配失败,立即扩大堆大小,g1的设计理念就是减少full gc

一键复制

编辑

Web IDE

原始数据

按行查看

历史

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值