JVM系列文章目录
注:本文基于JDK1.8,是博主个人的JVM学习记录,错误的地方欢迎各位指正。
JVM对象
对象的创建过程
在JVM中对象创建如下:
-
加载类:这么没什么好解释的。
-
检查加载:简单点就是检查类是不是存在(检查符号引用)。
-
分配内存:JVM给对象分配内存空间有下面两种方法。
分配空间的两种方式指针碰撞
JVM使用指针碰撞的时候,针对的是内存空间规整的时候,这个时候给对象分配内存只要后移指针就可以了。
空闲列表
当内存空间不规整当时候,也就是东一块空闲内存西一块内存当时候,JVM从它自己维护的空闲列表中选择一个可以放下对象的内存区域分配给对象。
当然在JVM中不可能是就一个线程取创建对象,所以需要考虑到并发安全,当有多个线程同时申请给对象分配内存空间时,JVM有下面两种机制
CAS(Compare And Swap 比较交换)加失败重试
线程1执行时,先去内存中找空闲的一块地址取一个值old,然后预处理对象,然后再去那个地址取值与此前存储的old进行比较是否相等,如果相等证明没有其他线程修改就可以分配,不相等就重新找一块地址再往复这个过程。(这种方式还有预处理、比较这些操作,所以比较慢。)
本地线程分配缓存(TLAB):
这个算法很简单,就是来一个线程就给这个线程在堆中间分配一个专属空间,这个空间只有这个线程可以操作(这种方式性能更高,JVM默认是开启的)。
可以通过参数 -XX:+UseTLAB进行设置。允许在年轻代空间中使用线程本地分配块(TLAB)。默认情况下启用此选项。要禁用 TLAB,请设置-XX:-UseTLAB。
-
内存空间初始化:我们可以理解成给赋对象默认值。
-
设置:这里主要是设置对象头的信息:
对象内存布局如下图所示,这里不做过多解释,主要说明一下,由于JVM规定了对象大小必须是8字节的倍数,有的时候对象头+实际数据可能不是8字节的倍数,就需要用对齐填充来填一些没什么用的数据凑够这个8字节的倍数。 -
对象初始化:其实到上一步,在JVM中它自己已经认为对象初始化完成了,但是在我们Java开发的眼里,对象初始化才刚开始,所以这一步实际上是根据我们程序代码的设置给对象进行赋值(注意与内存初始化区分,那里的赋值是初始值,比如说我们设置一个int类型,在代码中没有给值,这个int类型也会有一个默认值,这个默认值就是在对象内存初始化的时候赋的)。
对象的访问方式
JVM中对象访问方式分两种:
-
句柄
这种方式是在堆中间申请一块内存区域作为句柄池,句柄池里存储了对象的具体地址信息,栈中的reference指向的是句柄池地址。通俗点就是类似于地址簿的功能,你要找一个人,你不知道他的具体地址,但是你知道你家地址簿里面有记录,你在地址簿中能找到那个人的地址(这种方式特点是稳定,因为不是直接指向对象地址,但是正因为要要通过句柄池才能拿到对象地址,所以效率低,大部分JVM都不用这种方式)。
-
直接指针
从字面上理解就是栈中的reference直接指向对象的地址(这种方式特点是高效,JVM基本上使用这种方式)。
判断对象的存活
判断对象存活之前,我们需要先理解,什么是垃圾?垃圾其实跟我们生活中的理解一样,就是没有用的东西(当然这个比喻不是很贴切,但大致意思差不多)。那JVM是怎么判断对象是垃圾的呢?这里介绍两种方法:
- 引用计数器
这种方式就是对象每被引用一次,计数器就+1,该引用消失了就-1。这种方式有一个缺点就是没办法解决循环引用(a对象引用b对象,b对象又引用a对象)的问题。 - 可达性分析
可达性分析,可能很多做Java开发的人都听说过,或者说叫根可达。那么在介绍可达性分析前我们需要先知道JVM中是怎么定义根(GC roots)的。
GC roots:
我们日常主要是前面这4种- 虚拟机栈中的变量对象(栈帧中的局部变量表)。
- 方法区中的常量对象(比如:字符串常量池里的引用)。
- 方法区中的静态对象(java类的引用类型静态变量)。
- 本地方法JNI(一般是native修饰的方法)对象。
- JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。
- 所有被同步锁(synchronized关键)持有的对象。
- JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象,这个后续博客会介绍)。
这里再额外先介绍一下class的回收
类一般很难回收,它的回收条件相当的苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制)。
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。(除非你是自定义的类加载器,所以我觉得这一个条件就已经pass了)。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 参数控制:
-Xnoclassgc
Finalize
这里在介绍一下Finalize方法,这种方法能在对象被GC前救对象一命,但是在第二次gc的时候就没用了,有点像免死金牌,可保一命。而且这个方法有一个确定,优先级超级低,需要当前线程等一段时间,也就是说JVM要GC的时候你让JVM等一段时间,或者在它GC之前一段时间先触发了,这就很扯淡了,太不可控了,所以基本上不会用这个方法,要实现类似的功能我们可以使用try-finally
。下面是代码解释这个方法:
public class FinalizeGC {
public static FinalizeGC instance = null;
public void isAlive(){
System.out.println("想不到吧?我还活着!");
}
@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("免死金牌发动");
FinalizeGC.instance = this;
}
public static void main(String[] args) throws Throwable {
instance = new FinalizeGC();
//对象进行第1次GC
instance =null;
System.gc();
Thread.sleep(1000);//Finalizer方法优先级很低,需要等待
if(instance !=null){
instance.isAlive();
}else{
System.out.println("啊!我死了!");
}
//对象进行第2次GC
instance =null;
System.gc();
Thread.sleep(1000);
if(instance !=null){
instance.isAlive();
}else{
System.out.println("啊!我死了!");
}
}
}
下面是执行结果
对象的分配策略
-
优先分配在新生代的Eden区。
所有的对象几乎都分配在堆空间,那这句话都意思就是还有一扭扭对象不在堆空间,那它们在哪里呢?在虚拟机栈,这里涉及到JVM的一个分配策略逃逸分析——JVM在进行优化的时候,会分析方法中的对象能否逃逸,有没有其被外部方法所引用(比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。)。如果没有逃逸,对象又比较小,就直接分配在栈里(方法会封装成栈帧压入虚拟机栈,这个对象会作为一部分,栈帧可以参考 初识JVM)。 -
大对象直接进入老年代
这个就是字面上的意思,大对象直接就分配到老年代(这么做主要就是GC的时候减少复制对象开销,老让一个大对象在新生代各个区域晃荡,要复制来复制去,很浪费资源的),可以通过-XX:PretenureSizeThreshold
来设置,内存大于多少的对象直接进入老年代。 -
长期存活的对象进入老年代
在上面介绍对象内存布局的时候描述了对象头,对象头里有一个叫GC分代年龄的东西,这个是存储的二进制值,最大是1111(十进制为15),是用来存储对象的存活时间的。搞清楚这个之后我再来介绍这个年龄是怎么计算的。
对象a在进入Eden区的时候这个年龄记录是0
Eden当发生一次复制回收GC后,如果这个对象不能被回收的话,就会进入到From区或者To区,年龄+1;
Eden又发生一次复制回收GC后,如果这个对象还不能被回收的话,它会跟随那些不能被回收的对象,统一分配到From区或者To区,年龄+1;
以此类推,当a年龄到达15的时候,它就进入老年代了
-
动态对象年龄判断。
这里指的是在Form区或者To区里,这个区,相同年龄的对象总内存大小,大于这个区的内存大小的1/2,那么这些对象全都移到老年代。比如说在From区年龄等于4的对象占内存80M,而From区大小150M,那么这个时候年龄大于或等于4的对象就会统一进入老年代 -
空间分配担保
类似于银行贷款担保人,银行会先检查担保人担保资质,如果贷款人跑路了,那就有担保人来偿还。当然这个比如也不是很精确,这里来引入一个更精确的描述——在发生 Minor GC (新生代GC)之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全 的。如果不成立,则虚拟机会查看HandlePromotionFailure
设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历 次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的,如果担保失败则会进行一次 Full GC;如果小 于,或者HandlePromotionFailure
设置不允许冒险,那这时也要改为进行一次 Full GC。
整体分配流程看起来就是这样的:
JVM引用
引用的区分
JVM中引用分成四种:
- 强引用
=
:这就是我们常用的等号,这种强引用在根可达的情况下,就算OOM也不会被回收。 - 软引用
SoftReference
:这种就比强引用弱了,就算在根可达的情况下,如果即将发生OOM了,就会被回收(可以用在缓存技术上)。下面是代码演示,先设置VM参数-Xms20m -Xmx20m
public class TestSoftRef {
//对象
public static class User{
public String name = "";
public int age = 0;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
}
//
public static void main(String[] args) {
User u = new User("羽毛",18); //new是强引用
SoftReference<User> userSoft = new SoftReference<User>(u);//软引用
u = null;//干掉强引用,确保这个实例只有userSoft的软引用
System.out.println(userSoft.get()); //看一下这个对象是否还在
System.gc();//进行一次GC垃圾回收 千万不要写在业务代码中。
System.out.println("gc之后");
System.out.println(userSoft.get());
//往堆中填充数据,导致OOM
List<byte[]> list = new LinkedList<>();
try {
for(int i=0;i<100;i++) {
list.add(new byte[1024*1024*1]); //1M的对象 100m
}
} catch (Throwable e) {
//抛出了OOM异常时打印软引用对象
System.out.println("Exception*************"+userSoft.get());
}
}
}
下面是运行结果
- 弱引用
WeakReference
:比软引用更弱,就算在根可达的情况下,只要发生GC就会被回收(比软引用更适合用在缓存技术上)。
public class TestWeakRef {
public static class User{
public String name = "";
public int age = 0;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
}
public static void main(String[] args) {
User u = new User("羽毛",18);
WeakReference<User> userWeak = new WeakReference<User>(u);
u = null;//干掉强引用,确保这个实例只有userWeak的弱引用
System.out.println(userWeak.get());
System.gc();//进行一次GC垃圾回收,千万不要写在业务代码中。
System.out.println("gc之后");
System.out.println(userWeak.get());
}
}
下面是运行结果
- 虚引用
PhantomReference
:又叫幽灵引用这简直是弱鸡,基本上没人用,随时都有可能被回收掉,太不可控了。
上一篇:继续深入JVM内存区域
下一篇:JVM分代回收机制和垃圾回收算法