Java JVM堆空间的概述
1.设置堆空间初始值和最大值
通过idea 的-VM参数设置,如下图:
2.堆的核心概述
内存细分
3.堆空间大小的设置
-
详情请看 Java SE8 官方文档 Ctrl+F 搜索 -Xms
-
通过JVM参数来设置堆空间的初始值
- -Xms用来设置堆空间(年轻代+老年代)的初始内存大小
- -X是jvm的运行参数
- ms是memory start
-
通过JVM参数来设置堆空间的最大值
- -Xmx用来设置堆空间(年轻代+老年代)的最大内存大小
-
堆的初始大小(以字节为单位)。
- 此值必须是1024的倍数且大于1 MB。在字母后面加上
k
或K
表示千字节,m
或M
表示兆字节,g
或G
表示千兆字节。 - 以下示例说明如何使用各种单位将分配的内存大小设置为6 MB:
- -Xms6291456
- -Xms6144k
- -Xms6m
- 如果未设置此选项,则初始大小将设置为为老一代和年轻一代分配的大小之和。可以使用
-Xmn
选项或-XX:NewSize
选项设置年轻代的堆的初始大小。
- 此值必须是1024的倍数且大于1 MB。在字母后面加上
-
开发中建议将初始堆内存和最大的堆内存设置成相同的值。
-
打印GC过程的细节
- JVM参数设置:-XX:+PrintGCDetails
添加代码测试**
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
}
}
打印GC日志(详情信息看后期博客)
4.新生代与老年代
-
-XX:NewRatio:设置新生代与老年代的比例。
-
设置新老一代大小之间的比率。默认情况下,此选项设置为2。下面的示例演示如何将年轻/老人比率设置为1:1(这里就是年轻代占堆空间1:2,老年的占1:2)
-XX:NewRatio =3 //设置老年代占堆空间的3:4
-
-
-XX:SurvivorRatio:设置新生代中Eden区与survivor区的比例
-
-XX: -UseAdaptivesizePolicy :关闭自适应的内存分配策略( 暂时用不到)
-
Xmn:设置新生代的空间的大小。 ( 一般不设置)
5.图解对象分配的过程
6.常用调优工具
- JDK命令行
- Eclipse :Memory Analyzer Tool
- Jconsole
- Vi sual VM
- Jprofiler(暂时使用) 下载及破解
- Java Flight Recorder
- GCViewerGC Easy
7.Minor GC、Major GC与Full GC
-
年轻代GC(Minor GC) 触发机制:
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是
Eden代满,Survivor满不会引发GCI(每次Minor GC会清理年轻
代的内存。) - 因为Java对象大多都具备朝生夕灭的特性,所以MinorGC非常频
繁,一般回收速度也比较快。这一-定义既清晰又易于理解。 - Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线
程才恢复运行。
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是
-
老年代GC (Major GC/Fu11 GC)触发机制:
- 指发生在老年代的GC, 对象从老年代消失时,我们说“Major GC”或“Fu11 GC”
发生了。 - 出现了Major GC, 经常会伴随至少一.次的Minor GC (但非绝对的,在Paral1el
Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。- 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,
则触发Major GC
- 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,
- Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
- 如果Major GC后,内存还不足,就报00M了。
- Major GC的速度- -般会比Minor GC慢10倍以上。
- 指发生在老年代的GC, 对象从老年代消失时,我们说“Major GC”或“Fu11 GC”
-
Fu11 GC,触发Fu1l GC执行的情况有如下五种:
- 调用System. gc()时,系统建议执行Fu1l GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space0 (From Space)区向survivor space1 (To
Space)区复制时,对象大小大于To Space可 用内存,则把该对象转存到老年代,且
老年代的可用内存小于该对象大小;
说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。
8.堆空间分代思想
9.内存分配策略
-
如果对象在Eden(伊甸园区)出生并经过第一次MinorGC 后仍然存活,并且能被Survivor
容纳的话,将被移动到Survivor空间中,并将对象年龄设为1 。对象在
Survivor区中每熬过一次MinorGC ,年龄就增加1 岁,当它的年龄增加到- -定
程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代
中。 -
对象晋升老年代的年龄阈值,可以通过选项-XX: MaxTenuringThreshold来设置。
-
针对不同年龄段的对象分配原则如下所示:|
-
优先分 配到Eden
-
大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象长期存活的对象分配到老年代
-
动态对象年龄判断
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空
间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到
MaxTenur ingThreshold中要求的年龄。
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空
-
空间分配担保
- -XX: Handle PromotionFailure
-
10.对象分配过程: TLAB
-
为什么有TLAB ( Thread Local Allocation Buffer ) ?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内
存空间是线程不安全的; - 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
-
什么是TLAB ?
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为
每个线程分配了一个私有缓存区域,它包含在Eden空间内。 - 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,
同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称
之为快速分配策略。 - 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为
TLAB分配过程:
11.堆空间常用的VM参数
-
-XX: +PrintFlagsInitial :查看所有的参数的默认初始值
-
-XX:+PrintFlagsFinal : 查看所有的参数的最终值(可能会存在修改,
不再是初始值) -
-Xms:初始堆空间内存 (默认为物理内存的1/64)
-
-Xmx:最大堆空间内存(默认为物理内存的1 /4)
-
Xmn: 设置新生代的大小。(初始值及最大值)
-
XX: +DoEscapeAnalysis ; 开启逃逸分析
-
XX:NewRatio:配置新生代与老年代在堆结构的占比
-
-XX:SurvivorRatio:设置新生代中Eden和SO/S1空间的比例
-
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-
-XX: +PrintGCDetails:输出详细的GC处理日志
-
-XX:HandL ePromotionFailure:是否设置空间分配担保
-
-XX:+EliminateAllocations: 开启标量替换
12.通过逃逸分析看堆空间的对象分配策略
1.堆是分配对象存储的唯一选择吗?
- 在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导
致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。 - 在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一一
种特殊情况,那就是如果经过逃逸分析(Escape Analysis) 后发现,一个对象并没有
逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须
进行垃圾回收了。这也是最常见的堆外存储技术。 - 此外,前面提到的基于openJDK深度定制的TaoBaoVM,其中创新的GCIH (GC
invisible heap) 技术实现off -heap,将生命周期较长的Java对象从heap中移至
heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升
GC的回收效率的目的。
2.逃逸分析概述
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数
全局数据流分析算法。 - 通过逃逸分析,JavaHotspot编译器能够分析出一个新的对象的引用的
使用范围从而决定是否要将这个对象分配到堆上。 - 当能够明确对象不会发生逃逸时,就可以对这个对象做一个优化,不将其分配到堆上,而是直接分配到栈上,这样在方法结束时,这个对象就会随着方法的出栈而销毁,这样就可以减少垃圾回收的压力。
- 逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有
发生逃逸。 - 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃
逸。例如作为调用参数传递到其他地方中。
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有
/**
* 逃逸分析
*
* 如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用。
*/
public class EscapeAnalysis {
public EscapeAnalysis obj;
/*
方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
//思考:如果当前的obj引用声明为static的?仍然会发生逃逸。
/*
对象的作用域仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
//getInstance().xxx()同样会发生逃逸
}
}
3.逃逸分析:代码优化
使用逃逸分析,编译器可以对代码做如下优化:
一、栈上分配:
将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
二、同步省略:
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
三、分离对象或标量替换:
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
4.逃逸分析的缺点
- 关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技
术到如今也并不是十分成熟的。 - 其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分
析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复
杂的分析的,这其实也是一个相对耗时的过程。 - 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃
逸分析的过程就白白浪费掉了。 - 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
注意到有一些观点,认为通过逃逸分析,JVM会 在栈.上分配那些不会逃逸的对象,这
在理论上是可行的,但是取决于JVM设计者的选择。据我所知,Oracle Hotspot
JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的
对象实例都是创建在堆上。 - 目前很多书籍还是基于JDK 7以前的版本,JDK已经发生了很大变化,intern字符串.
的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,
intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以
这一点同样符合前面一点的结论:对象实例都是分配在堆上。