From 088a1b69e47a529d7a26c4cf001cbfbd1189a65a Mon Sep 17 00:00:00 2001 From: yang520-bye <78717426+yang520-bye@users.noreply.github.com> Date: Sun, 3 Oct 2021 11:26:58 +0800 Subject: [PATCH] docs: update 01-jvm-memory-structure.md (#21) Co-authored-by: Yang Libin Co-authored-by: Yang Libin --- docs/01-jvm-memory-structure.md | 71 ++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/01-jvm-memory-structure.md b/docs/01-jvm-memory-structure.md index cc6044a..679cc6c 100644 --- a/docs/01-jvm-memory-structure.md +++ b/docs/01-jvm-memory-structure.md @@ -136,7 +136,7 @@ Java 中任何一个普通方法都具备虚函数的特征(运行期确认, ### 堆的定义 -堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。 +堆是用来存放对象的内存空间,`几乎`所有的对象都存储在堆中。 ### 堆的特点 @@ -163,7 +163,7 @@ Java 中任何一个普通方法都具备虚函数的特征(运行期确认, ### 对象分配过程 - new 的对象先放在 Eden 区,大小有限制 -- 如果创建新对象时,Eden 空间填满了,就会触发 Minor GC,将 Eden 不再被其他对象引用的对象进行销毁,再加载新的对象放到 Eden 区 +- 如果创建新对象时,Eden 空间填满了,就会触发 Minor GC,将 Eden 不再被其他对象引用的对象进行销毁,再加载新的对象放到 Eden 区,特别注意的是 Survivor 区满了是不会触发 Minor GC 的,而是 Eden 空间填满了,Minor GC 才顺便清理 Survivor 区 - 将 Eden 中剩余的对象移到 Survivor0 区 - 再次触发垃圾回收,此时上次 Survivor 下来的,放在 Survivor0 区的,如果没有回收,就会放到 Survivor1 区 - 再次经历垃圾回收,又会将幸存者重新放回 Survivor0 区,依次类推 @@ -171,6 +171,73 @@ Java 中任何一个普通方法都具备虚函数的特征(运行期确认, jvm 参数设置次数 : -XX:MaxTenuringThreshold=N 进行设置 - 频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集 +### Full GC /Major GC 触发条件 + +- 显示调用`System.gc()`,老年代的空间不够,方法区的空间不够等都会触发 Full GC,同时对新生代和老年代回收,FUll GC 的 STW 的时间最长,应该要避免 +- 在出现 Major GC 之前,会先触发 Minor GC,如果老年代的空间还是不够就会触发 Major GC,STW 的时间长于 Minor GC + +### 逃逸分析 + +- #### 标量替换 + + - 标量不可在分解的量,java 的基本数据类型就是标量,标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在 JAVA 中对象就是可以被进一步分解的聚合量 + - 替换过程,通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM 不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。 + +- **对象和数组并非都是在堆上分配内存的** + +- 《深入理解 Java 虚拟机中》关于 Java 堆内存有这样一段描述:随着 JIT 编译期的发展与逃逸分析技术逐渐成熟,`栈上分配`,`标量替换`优化技术将会导致一些变化,所有的对象都分配到堆上也渐渐变得不那么"绝对"了。 + +- 这是一种可以有效减少 Java 内存堆分配压力的分析算法,通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 + +- 当一个对象在方法中被定义后,它可能被外部方法所引用,如作为调用参数传递到其他地方中,称为`方法逃逸`。 + +- 再如赋值给类变量或可以在其他线程中访问的实例变量,称为`线程逃逸` + +- 使用逃逸分析,编译器可以对代码做如下优化: + + - 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 + + - 将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。 + + - 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。 + +```java +public static StringBuffer createStringBuffer(String s1, String s2) { + + StringBuffer s = new StringBuffer(); + + s.append(s1); + + s.append(s2); + + return s; +} +``` + +s 是一个方法内部变量,上边的代码中直接将 s 返回,这个 StringBuffer 的对象有可能被其他方法所改变,导致它的作用域就不只是在方法内部,即使它是一个局部变量,但还是逃逸到了方法外部,称为`方法逃逸`。 + +还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为`线程逃逸`。 + +- 在编译期间,如果 JIT 经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。 +- jvm 参数设置,`-XX:+DoEscapeAnalysis` :开启逃逸分析 ,`-XX:-DoEscapeAnalysis` : 关闭逃逸分析 +- 从 jdk 1.7 开始已经默认开始逃逸分析。 + +### TLAB + +- TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区,是属于 Eden 区的,这是一个线程专用的内存分配区域,线程私有,默认开启的(当然也不是绝对的,也要看哪种类型的虚拟机) + +- 堆是全局共享的, 在同一时间,可能会有多个线程在堆上申请空间,但每次的对象分配需要同步的进行(虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性)但是效率却有点下降 +- 所以用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自己的 TLAB,这样可以使得线程同步,提高了对象分配的效率 +- 当然并不是所有的对象都可以在 TLAB 中分配内存成功,如果失败了就会使用加锁的机制来保持操作的原子性 +- `-XX:+UseTLAB `使用 TLAB,`-XX:+TLABSize` 设置 TLAB 大小 + +### 四种引用方式 + +- 强引用:创建一个对象并把这个对象赋给一个引用变量 , 普通 new 出来对象的变量引用都是强引用,有引用变量指向时永远不会被垃圾回收,jvm 即使抛出 OOM,可以将引用赋值为 null,那么它所指向的对象就会被垃圾回收 +- 软引用:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用 +- 弱引用:非必需对象,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。 +- 虚引用:虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 + ## 方法区 ### 方法区的定义