本地方法栈和堆
- 根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
由此先来看看本地方法栈和堆的作用。
1、本地方法栈:Native Method Stacks
- 是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
- 本地方法是用C/C++语言实现的。
- 和虚拟机栈类似,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
- 当某个线程调一个本地方法时,它就进入了一个新的并且不再受虚拟机限制的世界,它和虚拟机拥有同样的权限。
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
- 它可以直接使用本地处理器中的寄存器。
- 直接从本地内存的堆中分配任意数量的内存。
2、堆:Heap
2.1 堆的内存细分
- 现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
- jdk7及之前堆内存在逻辑上分为新生区+养老区+永久区。
- jdk8及之后堆内存在逻辑上分为新生区+养老区+元空间。
- 约定:新生区=新生代=年轻代;养老区=老年区=老年代;永久区=永久代。
例子1:
1 | public class HeapDemo { |
设置堆内存大小为:-Xms10m -Xmx10m。
运行后查看jvisualvm中Visual Gc的信息如下:
例子2:
1 | public class SimpleHeap { |
在虚拟机参数上设置-Xms10m -Xmx10m -XX:+PrintGCDetails打印垃圾回收信息,运行此程序后控制台出现(测试jdk8的):
1 | Heap |
2.2 设置堆内存大小
- java堆用于存储对象实例,堆的大小在JVM启动时就设定好了,可以通过-Xmx和-Xms来设置。
- “-Xms”表示堆区的起始内存,等价于-XX:InitialHeapSize。
- “-Xmx”表示堆区的最大内存,等价于-XX:MaxHeapSize。
- 一旦堆中的内存超过“-Xmx”指定的最大内存时,就会抛出OutOfMemoryError异常。
- 通常会为-Xmx和-Xms配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
- 默认情况下:
- 初始内存大小:物理电脑内存大小 / 64。
- 最大内存大小:物理电脑内存大小 / 4。
例子1:测试默认堆内存
1 | public class HeapSpaceInitial { |
打印出的值为:
1 | -Xms : 180M |
例子2:测试自定义堆内存
1 | public class HeapSpaceInitial { |
在虚拟机参数中输入-Xms600m -Xmx600m设置堆内存大小,运行后结果为:
1 | -Xms : 575M |
原因:
在终端中输入jps查看所有运行的java进程:
1 | zhu@zhu-PC:~$ jps |
接着使用jstat -gc 14484命令查看进程的内存使用情况:
通过计算:(25600+25600+153600+409600)/1024=600M
(25600+153600+409600)/1024=575M
说明实际上S0C和S1C两者只会选一个去放对象。
例子3:测试OOM
1 | public class OOMTest { |
过段时间后,报错如下:
1 | Exception in thread "main" java.lang.OutOfMemoryError: Java heap space |
2.3 年轻代和老年代
- 年轻代可分为Eden空间、Survivor0空间和Survivor1空间(有时也叫作from区、to区)。
- 新生代和老年代在堆结构中的占比:
- 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3。
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5。
- 查看方式:
- 1.通过Visual GC。
- 2.通过运行jinfo -flag <进程名> <端口查看>。
- Eden空间、Survivor0空间和Survivor1空间的比例为8:1:1,需要显示加上-XX:SurvivorRatio=8。可通过-XX:SurvivorRatio调整。
- 几乎所有的java对象都是在Eden空间被new出来的。
- 绝大部分java对象的销毁都在新生代进行。
- 可以使用-Xmn设置新生代最大内存大小。
总结一些对于堆的设置:
1 | -XX:NewRatio : 设置新生代与老年代的比例。默认值是2. |
2.4 对象分配过程
- new出来的对象先放到Eden区,此区有大小限制。
- 当Eden区的空间占满时,程序又需要创建对象,此时JVM的垃圾回收器将对Eden进行垃圾回收(Minor GC),将Eden中不再被引用的对象进行销毁,再加载新的对象放到Eden区。
- 然后将Eden中的剩余对象转到幸存者0区。
- 如果再次触发垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 可以通过设置次数来规定对象什么时候去养老区,默认是15次。(-XX:MaxTenuringThreshold=
) - 当养老区内存不足时,再次触发GC(Major GC),进行养老区内部清理。
- 当养老区执行GC后,发现依然无法进行对象的保存,就会报OOM异常。
图解过程:
- 当Eden区满时会触发YGC(红色代表垃圾,绿色代表还在被占用),此时红色部分的垃圾会被清除,而绿色部分的数据会被放进S0区(1代表JVM为每个对象分配的年龄计数器),可见当此次GC结束后,Eden区被清空,而所有对象被转到了S0区。(此时S1为to区)
- 当Eden区又满时会触发YGC,因为此时S1区是空的,所以Eden区中不是垃圾的部分会被转到S1区(而不是S0区),而这些对象的年龄计数器的值为1;同时S0区的对象还需要被判断是否还被使用,并将不被使用的对象清除和将还被使用的对象也转移进S1区,此时这些对象的年龄计数器的值为2。(此时S0为to区)
- 当Eden区再满时会触发YGC,此时Eden区中不是垃圾的对象被放进空的S0区(to区),S1中不是垃圾的对象也放进S0,区,并更新年龄计数器的值。注意:此时S1区中有两个对象的年龄计数器的值已经达到阈值(默认为15,可以通过参数-XX:MaxTenuringThreshold=
进行设置 ),所以它们不再在S0和S1区中转移,而是直接被放进老年代,并更新年龄计数器的值为16。
- 总结:
- 对于幸存者S0和S1,复制之后有交换,谁空谁是to。
- 对于垃圾回收,频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
- 触发YGC的情况只在Eden区满时;而S0和S1区满时不会触发,它们是被动触发。
对象分配的特殊情况:
2.5 各种垃圾收集器
- JVM在进行GC时,并非每次都对新生区、老年区、方法区一起回收,大部分时候回收的是指新生代。
- 对于HotSpot虚拟机,里面的GC按照回收区域分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)。
- 部分收集:不是完整收集整个java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代(Eden、S0、S1)的垃圾收集。
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集。
- 目前,只有CMS GC会有单独收集老年代的行为。
- 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集器。
- 目前,只有G1 GC会有这种行为。
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
- 部分收集:不是完整收集整个java堆的垃圾收集。其中又分为:
2.5.1 Minor GC(新生代GC)
- 当年轻代空间不足时,就会触发MInor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存)
- 因为java对象大多都都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
2.5.2 Major GC(老年代GC)
- 指发生在老年代的GC,对象从老年代消失时,我们说”Major GC”或”Full GC”发生了。
- 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
- 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC。
- Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
- 如果Major GC后,内存还不足,就报OOM了。
2.5.3 Full GC
- 触发Full GC执行的情况有如下五种:
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行。
- 老年代空间不足。
- 方法区空间不足。
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
- 由Eden区、S0(From Space)区向S1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
- Full GC是开发或调优中尽量要避免的,这样暂停时间会短一些。
2.5.4 垃圾收集器的测试
1 | public class GCTest { |
设置虚拟机参数:-Xms9m -Xmx9m -XX:+PrintGCDetails
运行结果:
1 | [GC (Allocation Failure) [PSYoungGen: 1918K(新生代GC前的内存大小)->504K(新生代GC后的内存大小)(2560K(新生代总空间,即2048+512))] 1918K(GC前占用堆的内存大小)->772K(比504K多,说明有些放到老年代了)(9728K(堆的总内存大小)), 0.0022849 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
2.6 内存分配策略
- 内存分配策略又叫做对象提升规则。
- 针对不同年龄段的对象分配原则如下所示:
- 优先分配Eden。
- 大对象直接分配到老年代。
- 尽量避免程序中出现过多的大对象。
例子:
1 | public class YoungOldAreaTest { |
配置虚拟机参数为:-Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails,此时Eden区为16m,S0区2m,S1区为2m,老年代区为40m。
打印结果如下:
1 | Heap |
从ParOldGen total 40960K, used 20480K的信息可知程序中定义的大数组直接分配到了老年代。
- 长期存活的对象分配到老年代。
- 动态对象年龄判断。
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
2.7 TLAB
- TLAB(Thread-local allocation buffer)的重要性?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
- 什么是TLAB?
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
- 据我所知所有openJDK衍生出来的JVM都提供了TLAB的设计。
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
- 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间,默认是开启的。查看是否开启,可在程序运行时在命令行输入jps并输入jinfo -flag UseTLAB <端口号>查看。
- 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
2.8 堆空间的参数设置
- -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值。
- -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)。
- 具体查看某个参数的指令: jps:查看当前运行中的进程;jinfo -flag SurvivorRatio 进程id。
- -Xms:初始堆空间内存 (默认为物理内存的1/64)
- -Xmx:最大堆空间内存(默认为物理内存的1/4)
- -Xmn:设置新生代的大小。(初始值及最大值)
- -XX:NewRatio:配置新生代与老年代在堆结构的占比。
- -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例。
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄。
- -XX:+PrintGCDetails:输出详细的GC处理日志。
- 打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
- -XX:HandlePromotionFailure:是否设置空间分配担保。
- 在发生 Minor gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有
对象的总空间。- 如果大于,则此次 Minor gc是安全的。
- 如果小于,则虚拟机会查看-XX: HandlePromotionfailure设置值是否允许担保失败。
- 如果 HandlePromotionfailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
- 如果大于,则尝试进行一次 Minor gc,但这次 Minor gc依然是有风险的。
- 如果小于,则改为进行一次FullGC。
- 如果HandlePromotionfailure=false,则改为进行一次FullGc。
- 如果 HandlePromotionfailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
- 注意,JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor gc,否则将进行Full Gc。JDK6 Update24之后(JDK7),,HandlePromotionfailure参数不会再影响到虛拟机的空间分配担保策略,也就是一直HandlePromotionfailure=true。
- 在发生 Minor gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有
2.9 逃逸分析
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
- 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
- 通过逃逸分析, Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
- 逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
- 在JDK6u23版木之后, Hotspot中默认就已经开启了逃逸分析。如果使用的是较早的版本,开发人员则可以通过选项“-xx: +DoEscapeAnalysis显式开启逃逸分析,通过选项“-XX: +PrintEscapeAnalysis”査看逃逸分析的筛
选结果。
例子:
1 | public class EscapeAnalysis { |
2.9.1 代码优化之栈上分配
- JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
例子:
1 | public class StackAllocation { |
JVM参数为:-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails,其中**-XX:-DoEscapeAnalysis**为不开启逃逸分析(默认打开)。
执行结果:
1 | 花费的时间为: 127 ms |
打开JvisualVM查看信息:
去掉**-XX:-DoEscapeAnalysis**后执行结果:
1 | 花费的时间为: 20 ms |
2.9.2 代码优化之同步省略
- 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
- 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的
同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
2.9.3 代码优化之标量替换
- 标量是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
- 相对的,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
- 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
例子:
1 | public class ScalarReplace { |
JVM参数为:-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations,其中-XX:-EliminateAllocations参数为不打开标量替换(默认打开,允许将对象打散分配在栈上)。
打印结果为:
1 | [GC (Allocation Failure) 25600K->488K(98304K), 0.0014283 secs] |
打开标量替换后运行结果为:
1 | 花费的时间为: 9 ms |
因为对象被打散在栈上,所以没有出现GC。