对象内存分配
一个对象在进行内存分配的过程是比较复杂的,我们一般都理解为 JVM 在进行对象内存分配的时候会将对象直接分配到堆上,其实不然,真实的流程是 JVM 会尝试先在栈上分配,栈上无法分配时才会去到堆上分配,整体流程看下图。
栈上分配
对于对象栈上分配,它可以缓解堆内存的压力以及 gc 的次数。并不是所有的对象都会尝试到栈上去分配,JVM 会对其进行逃逸分析
逃逸分析
逃逸分析的过程就是在寻找逃逸对象和非逃逸对象。非逃逸对象即在方法内的对象其没有在外部被使用到,只在方法中被调用使用,其生命周期和方法本身是一样的,即方法结束后即可销毁,这类对象显然放在栈中随栈帧一起出栈销毁效率会更快,可以节省 gc 的次数。
public void func1(){
Run run=new Run();
run.setName("wo");
run.setWhyRun("her");
//未逃逸,可尝试进行栈上分配
}
public Run func2(){
Run run=new Run();
run.setName("wo");
run.setWhyRun("her");
//逃逸,堆内分配
return run;
}
标量替换
每个线程的栈内存空间相对于堆来说是很小的,如果栈内没有足够的连续内存空间一个对象的分配是很较难成功的,所以即使通过了逃逸分析也会存在大量的对象尝试栈上分配失败的情况,最后还是进行堆内分配。 所以一般 逃逸分析需要和标量替换同时使用。 标量替换即将一个对象的成员变量拆分出来分散存储,并标记他们所属的对象。这样即使没有连续的内存空间也可以进行栈上分配
标量和聚合量 标量即无法进一步分解的量,比如 Java 中基本数据类型,聚合量即可以被进一步分解的量,比如我们自定义的类等。
逃逸分析及标量替换在 JDK 1.7 之后是默认开启的 开启逃逸分析参数(-XX:+DoEscapeAnalysis) 开启标量替换参数(-XX:+EliminateAllocations)
堆内分配
对象在 Eden 区分配 一般来说一个对象在堆内进行分配时,通常都是分配到 Eden 区 ,当 Eden 区满时会触发 minor gc 会回收 eden 区 及 s0 及 s1 区内存空间。 大对象直接进入老年代 对于大对象来说在堆内存分配时会直接分配到老年代 如何来定义大对象呢?一种是对象大小大于 Eden 区大小,一种是我们进行了设置 JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。 长期存活对象进入老年代 在进行 minor gc 时,存活的对象会在 s0 区及 s1 区来回流转,每一次流转其分代年龄都会 +1 当分代年龄到达 15 时会将该对象放入老年代(一般默认为 15 不同垃圾收集器会稍有不同) 对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置 对象年龄动态判断机制 在进行 minor gc 时,如果存活对象 分代年龄 1+ 分代年龄 2 + 分代年龄 3 + 分代年龄 n 对象超过 s 区内存大小的 50% 则会将 分代年龄 n 及 n 以上的对象直接放到老年代。 老年代空间担保机制 在进行 minor gc 前 jvm 会进行判断如果 gc 前的 eden 区存储对象大小超过老年代剩余内存大小,则会判断是否设置了 “-XX:-HandlePromotionFailure”(jdk1.8默认设置),未设置则直接进行 full gc。如果设置则会判断当前老年代剩余空间是否小于历次进入老年代对象大小的平均值,如果小于则进行 full gc 否则进行 minor gc
需要注意的是,在jdk6 uptate 24之后,HandlePromotionFailure已经不起作用了,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。即默认担保无需设置
对象内存回收
在进行 gc 时 JVM 需要判断哪些对象是可以进行回收的垃圾对象。
引用计数
引用计数法,顾名思义,虚拟机会记录每个对象被引用的个数,如果在 gc 时某个对象被引用的个数为 0 则认为它时可以被回收的垃圾对象。引用计数法简单高效,但存在循环引用的情况会导致垃圾对象无法被回收,所以主流的虚拟机大都使用的时可达性分析算法
public class CountNumTest {
public Object innerObj;
public static void main(String[] args) {
CountNumTest objA = new CountNumTest();
CountNumTest objB = new CountNumTest();
objA.innerObj = objB;
objB.innerObj = objA;
objA = null;
objB = null;
}
}
可达性分析算法
以 GC Roots 为起点,从这些节点向下搜索,所有被引用的对象都被标记为非垃圾对象,其余对象为垃圾对象。 GC Roots :线程栈本地变量,静态变量,本地方法栈变量 等。
常见引用类型
强引用、软引用、弱引用、虚引用
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class ReferenceTest {
public static void main(String[] args) {
//强引用
String string = new String("strong");
//软引用
SoftReference<String> stringSoftReference = new SoftReference<String>("stringSoftReference");
//弱引用
WeakReference<String> stringWeakReference = new WeakReference<String>("stringWeakReference");
}
}
一般常用的是强引用和软引用。 强引用: 我们声明的一般对象,在 gc 时不会被回收 软引用:
SoftReference<String> stringSoftReference = new SoftReference<String>("stringSoftReference");
软引用在堆内存足够的时候不会被回收,但在内存不足无法释放空间时会被回收。其可作为内存敏感的高速缓存来使用。 弱引用 弱引用相当于没有引用,gc 时会直接回收掉,很少使用。
WeakReference<String> stringWeakReference = new WeakReference<String>("stringWeakReference");
虚引用 也称为幻影引用,幽灵引用,最弱的一种引用,几乎不用。
finalize() 方法最终判断对象存活
在 gc 回收时 会进行 两次标记 第一次标记 第一次标记对象并不一定就会对垃圾对象宣判“死刑”。至少要经历第二次标记才会最终确定。 第二次标记 在二次标记的时候会判断 对象是否覆盖了 finalize() 方法。对象在这个时候有在 finalize 方法中自救的机会。但 finalize() 方法的运行代价高昂, 不确定性大, 无法保证各个对象的调用顺序已经不推荐使用。这里作为了解就好。
如何判断一个类是否无用
一个类无用需要同时满足下面 3 个条件
- 该类的所有实例对象都已经被回收,即在 JVM 堆中没有该类的任何实例
- 加载该类的 ClassLoader 已被回收
- 该类的 java.long.Class 对象没有在任何地方被引用,且无法在任何地方通过反射生成该类或访问该类方法。