堆概述
一个JVM实例只存在一个 堆内存,堆也是Java内存管理的核心区域。 Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块 内存空间。 堆可以处于物理上不连续但是逻辑上连续 的内存空间中。 所有线程共享Java堆,还可以在这里划分线程私有的缓冲区 。
存储
几乎所有的对象实例和数组 都在堆上分配内存,因为栈帧中保存的是引用,引用指向对象或者数组在堆中的位置。 方法介绍后,堆中的对象不会被马上移出,而是在垃圾收集 的时候才会被移除。 堆,是GC执行垃圾回收 的重点区域。
内存细分
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区(永久代) Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间 永久区和元空间只是逻辑上属于堆空间,实际上在方法区里,所以这里暂时只讲新生区和养老区。
设置堆空间大小
"-Xms"用于表示堆区的起始内存 “-Xmx”用于表示堆区中的最大内存 一旦堆区中的内存大小超过"-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。 通常将以上两个参数配置相同的值,其目的是能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
OOM
针对堆空间,出现了内存溢出。
年轻代与老年代
老年代一般占2/3堆空间,新生代一般占1/3堆空间。 垃圾回收频繁在年轻代收集,很少在老年代中收集,几乎不在永久区/元空间中收集。
堆空间分代思想
分代的用处是:优化GC性能。 可以方便GC找到适合的区域。
年轻代
分为伊甸园区(eden space E区),survivor0区,surive1区(又称from区和to区),其中存储的只有E区和survive1区或0区,两个区必须有一个是空的。(内存占比8:1:1) 几乎所有的Java对象都是在Eden区被new出来的。 绝大部分的Java对象的销毁也都在新生代中进行。
对象分配的过程
new的对象先放在伊甸园区 当伊甸园区被填满,程序又需要创建对象时,JVM的垃圾回收器(Young GC)将对伊甸园区和S区进行垃圾回收,将伊甸园区中不再被其它对象所引用的对象进行销毁,将伊甸园中的剩余对象移动到survivor0区,再加载新的对象放到伊甸园区。 每个对象有一个年龄计数器,第一次被移动到s0区的age为1。 如果再次触发垃圾回收,此时把e区对象放在s1当中(空的s区)同时判断s0区中对象是否还在被使用,如果还在使用,则也放到s1区中。 故0区和1区,谁空谁为to区。 当有对象的年龄计数器高于阈值,默认为15,则直接promotion到老年代。 如果老年代内存不够,会触发Major GC,进行养老区内存清理。如果老年代执行GC后依然无法进行对象的保存,就会产生OOM异常。
特殊情况
如果对象太大了, 超过新生代的内存大小,考虑直接放到老年代。如果老年代也放不下,如果上限高于且其中有对象,执行Major GC,如果本身就不够,则直接OOM。 如果在Eden区执行YGC时,如果S区放不下该对象,则将其晋升到老年代
GC回收
Minor GC(Young GC) Major GC Full GC
回收区域
JVM主要对新生代,老年代,方法区进行垃圾回收,大部分时候回收的都是新生代。 部分收集:不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC) 只是新生代的垃圾收集。 老年代收集(Major GC)只是老年代的垃圾收集。
整堆收集:Full GC 收集整个java堆和方法区的垃圾收集。
Minor GC触发机制
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满就不会引发GC。 Minor GC非常频繁,回收速度也比较快。 Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
Major GC触发机制
对象从老年代消失时,即Major GC或Full GC发生了。 出现Major GC 经常会伴随至少一次的Minor GC(并非绝对)就是说当老年代空间不足的时候,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC。 Major GC的速度一般会比Minor GC慢10倍以上。 如果Major GC后,内存还不足,就报OOM了。
Full GC触发机制
调用System.gc()时,系统建议执行Full GC,但是不必然执行 老年代空间不足 方法区空间不足 通过Minor GC进入老年代的平均大小大于老年代的可用内存。 由Eden区 survivor From区向survivor To区复制时,对象大小大于其可用内存,就把该对象转存到老年代,且老年代的可用内存小于该对象大小。
内存分配策略
针对不同年龄段的对象分配原则如下:
优先分配到Eden 大对象直接分配到老年代 长期存活的对象分配到老年代
对象提升规则
如果对象在Eden出生并经过第一次Minor GC后仍然存货,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。 对象在Survivor区中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。
对象分配过程:TLAB
Thread Local Allocation Buffer:为每个线程单独分配一个缓冲区。 它是 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。 主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。
背景
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
定义
从内存模型的角度对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域 ,它包含在Eden空间里。 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此可以将这种分配方式称为快速分配策略
堆上对象分配到栈
需要使用逃逸分析手段 逃逸分析是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
基本行为
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其它地方中。
例子
这个例子里创建的sb有可能会在外部被调用,所以不能存放在栈中,得存放在堆空间中。
public static StringBuffer createStringBuffer ( String s1, String s2) {
StringBuffer sb = new StringBuffer ( ) ;
sb. append ( s1) ;
sb. append ( s2) ;
return sb;
}
以下即可:
public static StringBuffer createStringBuffer ( String s1, String s2) {
StringBuffer sb = new StringBuffer ( ) ;
sb. append ( s1) ;
sb. append ( s2) ;
return sb. toString ( ) ;
}
如何快速判断是否发生了逃逸分析,只要看new的对象实体是否有可能在方法外被调用。
总结
开发中能使用局部变量的,就不要使用在方法外定义。
代码优化
栈上分配
栈上分配:将堆分配转化为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
同步省略
同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 如下代码对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,因为每个线程用来加锁的hollis都是new出来的,不会是同一个,所以在JIT编译阶段就会被优化掉。优化成:
public void f ( ) {
Object hollis = new Object ( ) ;
synchronized ( hollis) {
sout ( hollis) ;
}
}
public void f ( ) {
Object hollis = new Object ( ) ;
sout ( hollis) ;
}
标量替换
分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。 标量:一个无法再分解成更小的数据的数据,Java中的原始数据类型就是标量,相对的,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量。 如果经过逃逸分析,发现一个对象不会被外界访问,经过优化就会把这个对象拆解成若干个其中包含的若干个成员变量来代替 ,这个过程就是标量替换。