一、堆的核心概述
1、堆(Heap)的核心概述:堆的唯一性
(1)一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
(2)Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
① 堆内存的大小是可以调节的。
(3)
(4)所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( Thread Local Allocation Buffer,TLAB)。
2、关于对象创建与GC的概述
(1)《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated )
① 我要说的是:“几乎”所有的对象实例都在这里分配内存。—从实际使用角度看的。
(2)数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向。对象或者数组在堆中的位置。
(3)在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
(4)堆,是GC ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。
3、堆的细分内存结构
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
(1)JDK7
① Young Generation Space 新生区 Young/New
新生区又被划分为 Eden 区和 survivor 区;
② Tenure generation space 养老区 old/Tenure
③ Permanent Space 永久区 Perm
(2)JDK8
① Young Generation Space 新生区 Young /New
② Tenure generation space 养老区 old/Tenure
③ Meta Space 元空间 Meta
JDK8之前与JDK8堆结构变化:
约定: 新生区 = 新生代 = 年轻代; 养老区 = 老年区 = 老年代; 永久区=永久代
二、
“一Xms"用于表示堆区的起始内存,等价于-XX: InitialHeapsize
“-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapsize
一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
通常会将-Xms和一Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回 收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
1 public class OOMTest {
2 public static void main(String[] args) {
3 ArrayList<Picture> list = new ArrayList<>();
4 while(true){
5 try {
6 Thread.sleep(20);
7 } catch (InterruptedException e) {
8 e.printStackTrace();
9 }
10 list.add(new Picture(new Random().nextInt(1024 * 1024)));
11 }
12 }
13 }
14
15 class Picture{
16 private byte[] pixels;
17
18 public Picture(int length) {
19 this.pixels = new byte[length];
20 }
21 }
三、年轻代与老年代
1、年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:
① 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速;
②
2、新生代与老年代比例
下面这个参数开发中一般不会调:
配置新生代与老年代在堆结构的占比。
-
-
- 默认 -XX:NewRation = 2,表示新生代占 1,老年代占2,新生代占整个堆的 1/3;
- 可以修改 -XX:NewRation = 4,表示新生代占1, 老年代占4,新生代占整个堆的 1/5;
-
3、新生代
(1)在 HotSpot 中,Eden 空间和另外两个空间缺省所占的比例是 8:1:1;
(2)当然开发人员可以通过选项 “-XX:survivorRatio” 调整整个空间比例,比如 -XX:survivorRation=8;
(3)几乎所有的 Java 对象都是在 Eden 区被new出来的;
(4)绝大部分的 Java 对象的销毁都在新生代进行了;
① IBM 公司的专门研究表明,新生代中的 80% 的对象都是 "朝生夕死"的;
(5)可以使用选项 "-Xmn" 设置新生代最大内存空间;
① 这个参数一般使用默认值就可以了;
4、常用设置参数
-XX:NewRatio : 设置新生代与老年代的比例。默认值是2,新生代 :老年代=1 :2
-XX:SurvivorRatio=8 :设置新生代中Eden区与Survivor区的比例。默认值是8 需要手动设置(因为有自适应的内存分配策略)
-XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略 (暂时用不到)
-Xmn:设置新生代的空间的大小。 (一般不设置)如果也设置了NewRatio以 Xmn为准
jps 查看系统中运行的 java 进程
jinfo -flag SurvivorRation 线程id 查看比例Eden和Survivor比例
jinfo -flag NewRation 线程id 查看新生代与老年代比例
四、
当伊甸园区满的时候,会把伊甸园区和幸存者区一起进行回收;
2、幸存者区有可能直接到老年代;
3、针对幸存者S0,S1区的总结;复制之后有交换,谁空谁是to;
4、
1 public class HeapInstanceTest {
2 byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
3
4 public static void main(String[] args) {
5 ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
6 while (true) {
7 list.add(new HeapInstanceTest());
8 try {
9 Thread.sleep(10);
10 } catch (InterruptedException e) {
11 e.printStackTrace();
12 }
13 }
14 }
15 }
五、
触发Full GC执行的情况有如下五种:
(1)调用system.gc()时,系统建议执行Full GC ,但是不必然执行;
六、
七、
如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被 survivor 容纳的话,将被移动到 survivor 空间中,并将对象年龄设为1 。
对象在survivor 区中每熬过一次 MinorGC ,年龄就增加1岁,当它的年龄增加到一定程度(默认为15 岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过选项 -XX:MaxTenuringThreshold 来设置。
1 public class YoungOldAreaTest {
2 public static void main(String[] args) {
3 byte[] buffer = new byte[1024 * 1024 * 20];//20m
4
5 }
6 }
-Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
日志信息:
在这里可以看到直接把这个大对象分配到了老年代。
八、
1 public class TLABArgsTest {
2 public static void main(String[] args) {
3 System.out.println("我只是来打个酱油~");
4 try {
5 Thread.sleep(1000000);
6 } catch (InterruptedException e) {
7 e.printStackTrace();
8 }
9 }
10 }
从这里也可以看到 TLAB 默认情况下是开启的。
九、
-XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数的指令: jps:查看当前运行中的进程
jinfo -flag SurvivorRatio 进程id
-Xms:初始堆空间内存 (默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比(默认比例1:2)
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例(默认值是8)
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保
空间分配担保策略是什么?
(1)在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间;
① 如果大于,则此次Minor GC是安全的;
② 如果小于,则虚拟机会查看-XX :HandlePromotionFailure设置值是否允许担保失败。
a、如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
【1】如果大于,则尝试进行一次Minor GC,但这次Minor cc依然是有风险的:
【2】如果小于,则改为进行一次Full GC。
b、如果HandlePromotionFailure=false,则改为进行一次Full GC。
(2)在JDK6 Update24(JDK7)之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
十、
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH (GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升Gc的回收效率的目的。
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
(3)通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
(4)逃逸分析的基本行为就是分析对象动态作用域:
① 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
② 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
案例一:
1 public void my_method() {
2 V v = new V();
3 // use v
4 // ...
5 v = null;
6 }
没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。
案例二:
1 /**
2 * 发生逃逸:不能栈上分配
3 * @param s1
4 * @param s2
5 * @return
6 */
7 public static StringBuffer createStringBuffer(String s1, String s2) {
8 StringBuffer sb = new StringBuffer();
9 sb.append(s1);
10 sb.append(s2);
11 return sb;
12 }
13
14 //上述代码如果想要 StringBuffer sb 不逃出方法,可以这样写:
15
16 /**
17 *
18 * 未发生逃逸
19 * @param s1
20 * @param s2
21 * @return
22 */
23 public static String createStringBuffer2(String s1, String s2) {
24 StringBuffer sb = new StringBuffer();
25 sb.append(s1);
26 sb.append(s2);
27 return sb.toString();
28 }
1 /**
2 * 逃逸分析
3 *
4 * 如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用。
5 */
6 public class EscapeAnalysis {
7 public EscapeAnalysis obj;
8
9 /**
10 *方法返回EscapeAnalysis对象,发生逃逸
11 */
12 public EscapeAnalysis getInstance(){
13 return obj == null? new EscapeAnalysis() : obj;
14 }
15
16 /**
17 * 为成员属性赋值,发生逃逸
18 */
19 public void setObj(){
20 this.obj = new EscapeAnalysis();
21 }
22
23 //思考:如果当前的obj引用声明为static的?仍然会发生逃逸。
24
25 /**
26 *对象的作用域仅在当前方法中有效,没有发生逃逸
27 */
28 public void useEscapeAnalysis(){
29 EscapeAnalysis e = new EscapeAnalysis();
30 }
31
32 /**
33 * 引用成员变量的值,发生逃逸
34 */
35 public void useEscapeAnalysis1(){
36 EscapeAnalysis e = getInstance();
37 //getInstance().xxx()同样会发生逃逸
38 }
39 }
如果使用的是较早的版本,开发人员则可以通过:选项“ 一XX: +DoEscapeAnalysis"(+号表示开启,-号表示关闭)显式开启逃逸分析通过选项“一XX: +PrintEscapeAnalysis"查看逃逸分析的筛选结果。
结论:开发中能使用局部变量的,就不要使用在方法外定义。
(1)栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
(2)同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
(3)分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cPu寄存器中。
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。
分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的栈上分配的场景
在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。
代码测试:
1 public class StackAllocation {
2 public static void main(String[] args) {
3 long start = System.currentTimeMillis();
4
5 for (int i = 0; i < 10000000; i++) {
6 alloc();
7 }
8 // 查看执行时间
9 long end = System.currentTimeMillis();
10 System.out.println("花费的时间为: " + (end - start) + " ms");
11 // 为了方便查看堆内存中对象个数,线程sleep
12 try {
13 Thread.sleep(1000000);
14 } catch (InterruptedException e1) {
15 e1.printStackTrace();
16 }
17 }
18
19 private static void alloc() {
20 User user = new User();//未发生逃逸
21 }
22
23 static class User {
24
25 }
26 }
运行时可以使用如下参数进行测试:
-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
线程同步的代价是相当高的,同步的后果是降低并发性和性能。
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
代码测试:
1 public class SynchronizedTest {
2 public void f() {
3 Object hollis = new Object();
4 synchronized(hollis) {
5 System.out.println(hollis);
6 }
7 }
8
9 //代码中对 hollis 这个对象进行加锁,但是 hollis对象的声明周期只在f()方法中,
10 //并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成:
11 public void f2() {
12 Object hollis = new Object();
13 System.out.println(hollis);
14 }
15 }
标量(Scalar)是指一个无法博分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量(Aggregate) ,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
例如:
1 public static void main(String[] args) {
2 alloc();
3 }
4
5 private static void alloc() {
6 Point point = new Point(1, 2);
7 System.out.println("point.x = " + point.x + ";point.y=" + pont.y) ;
8 }
9
10 class Point {
11 private int x;
12 private int y;
13 }
以上代码,经过标量替换后,就会变成:
1 private static void alloc() {
2 int x= 1;
3 int y = 2;
4 System.out.println("point.x = " + point.x + ";point.y=" + pont.y) ;
5 }
可以看出,Point这个聚合量经过逃逸分析后,发现它并没有逃逸,就被替换成两个聚合量了。
那么标量替换有什么好处呢?
就是可以大大减少堆内存的占用,因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
标量替换为栈上分配提供了很好的基础。
标量替换参数设置:
参数:-XX:EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。
代码测试:
1 public class ScalarReplace {
2 public static class User {
3 public int id;
4 public String name;
5 }
6
7 public static void alloc() {
8 User u = new User();//未发生逃逸
9 u.id = 5;
10 u.name = "www.atguigu.com";
11 }
12
13 public static void main(String[] args) {
14 long start = System.currentTimeMillis();
15 for (int i = 0; i < 10000000; i++) {
16 alloc();
17 }
18 long end = System.currentTimeMillis();
19 System.out.println("花费的时间为: " + (end - start) + " ms");
20 }
21 }
可以设置运行参数:
-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
上述代码在主函数中进行了1亿次alloc。调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB。如果堆空间小于这个值,就必然会发生GC。使用如上参数运行上述代码:
① 参数-server:启动server模式,因为在server模式下,才可以启用逃逸分析;
② 参数一Xx:+DoEscapeAnalysis:启用逃逸分析;
③ 参数-xmx10m:指定了堆空间最大为10MB;
④ 参数-xx:+PrintGc:将打印Gc日志;
⑤ 参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配。
5、逃逸分析总结
(1)参数-server:启动server模式,因为在server模式下,才可以启用逃逸分析。
(2)逃逸分析并不成熟
① 关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。
② 其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
③ 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
④ 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据资料提供,Oracle HotspotJVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
⑤ 目前很多书籍还是基于JDK 7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。
1、年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
2、老年代放置长生命周期的对象,通常都是从survivor区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。
3、当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGC。当GC发生在老年代时则被称为MajorGC或者FullGC。一般的,MinorGC的发生频率要比MajorGC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。