深入了解JVM的内存机制和垃圾回收机制

对象内存分配机制

在这里插入图片描述
对象内存可在两个地方进行分配,一个是栈,一个是堆。
对象栈上的分配
JVM通过逃逸分析确定该对象不会被外部访问,如果不会逃逸就可以将该对象在栈上分配内存,这样该对象所占用内存空间就可以随栈帧出栈而销毁减少垃圾回收的压力
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。

1	public User test1() {
2 		User user = new User();
3 		user.setId(1);
4 		user.setName("zhuge");
5 		//TODO 保存到数据库
6 		return user;
7	}
8
9	public void test2() {
10 		User user = new User();
11 		user.setId(1);
12 		user.setName("zhuge");
13 		//TODO 保存到数据库
14	}

我们可以看到test1方法中的user对象被返回了,这个对象的作用域不确定,test2方法中的user对象我们可以确定当方法结束,这个对象就可以认为是无效对象,对于test2中的user对象JVM就会在栈中为其分配内存,让其在方法结束时跟随栈内存一起被回收掉。
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配到栈上,JDK7以后默认开启逃逸分析,如果要关闭,适用参数(-XX:-DoEscapeAnalysis)
标量替换:通过逃逸分析确定该对象不会被外部访问,并且能够进一步分解时,JVM不会创建该对象,而是将对象成员变量分解若干个被这个方法使用的成员变量,比如我们将上面的User对象分解为ID和Name,然后将ID和Name在栈帧或寄存器是上分配空间,这样就不会因为没有一大块连续的空间导致这个对象内存不够分配。开启标量替换(-XX:+EliminatteAllocations),JDK7之后默认开启
标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而在JAVA中对象就是可以被进一步分解的聚合量
栈上分配的实例

1	/**
2 	* 栈上分配,标量替换
3 	* 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
4 	*
5 	* 使用如下参数不会发生GC
6 	* ‐Xmx15m ‐Xms15m ‐XX:+DoEscapeAnalysis ‐XX:+PrintGC 				‐XX:+EliminateAllocations
7 	* 使用如下参数都会发生大量GC
8 	* ‐Xmx15m ‐Xms15m ‐XX:‐DoEscapeAnalysis ‐XX:+PrintGC ‐XX:+EliminateAllocations
9 	* ‐Xmx15m ‐Xms15m ‐XX:+DoEscapeAnalysis ‐XX:+PrintGC ‐XX:‐EliminateAllocations
10 	*/
11	public class AllotOnStack {
12
13 		public static void main(String[] args) {
14 			long start = System.currentTimeMillis();
15 			for (int i = 0; i < 100000000; i++) {
16 			alloc();
17 		}
18 		long end = System.currentTimeMillis();
19 		System.out.println(end ‐ start);
20 		}
21
22 		private static void alloc() {
23 			User user = new User();
24 			user.setId(1);
25 			user.setName("zhuge");
26     }
27	}

结论:栈上分配依赖于逃逸分析和标量替换

对象堆上的分配分为Eden区,Survicor(S1和S2区域)和Old区
先了解两个概念:
Minnor GC/Young GC:指发生在新生代的垃圾收集动作,MinorGC会非常的频繁,回收速度也会比较快。
Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾,Full GC会比Minor GC 慢十倍不止

- 对象在Eden区分配
大量的对象被分配到Eden区,当Eden区满了,就会触发Minor GC,剩余存活的对象就会被放到Survivor去,当下次Eden区满时,接着执行Minor GC然后回收Eden区和Survivor区。因为Eden区的对象都是朝生夕死,存活时间很短, 所以我们尽量让Eden区内存足够大,Survivor区只要够用就行了,JVM默认设置的内存比例的8:1:1是很合适的,可以通过-XX:+USeAdaptiveSizePolicy(默认开启),会根据JVM根据你的垃圾回收之后的结果自动调整这个比例,如果这个比例有改变,可以设置参数-XX:-UseAdaptiveSizePolicy
2.对象在Old区分配
一般我们的对象刚开始初始化之后都是放在Eden区的,不会直接到老年代,有以下这几种情况会导致对象直接存放到老年区

  • 大对象直接进入老年代
    大对象就是需要大量连续内存空间的对象(比如字符串,数组),JVM参数-XX:PretenureSizeThreshold可以设置大对象的大小,如果对象超过了设置大小,就会直接进去老年代,不会进去年轻代,这个参数只有在Serial和ParNew两个垃圾收集器下有效

  • 长期存活的对象进入老年代
    既然虚拟机采用了分代收集思想来管理内存,那么内存回收时就必须要识别那些对象应放在新生代,那些对象应放在老年代中。为了区别,JVM给每个对象一个对象年龄(Age)计数器。
    如果对象在Eden区出生并经过一次MinorGC之后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor区,并将对象年龄设为1,之后对象熬过一次MinorGC,年龄就增加1,当增加到一定年龄,就会被挪到老年代去(默认是15岁,CMS收集器默认是6岁,不同的垃圾收集器会略微有点不同)
    可以通过JVM参数:-XX:MaxtenuringThreshold 设置年龄

  • 对象动态年龄判断
    当前放对象的Survivor区域中(其中一块区域),一批对象的总大小超过了这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就直接进去老年代
    例如Survivor区域现在有一批对象,年龄1+年龄2+年龄3+年龄n的对象,他们大小超过了50%,此时就会把年龄n及以上的对象都放入老年代中,这个规则是为了让长期存活的对象早点进去老年代,对象动态年龄判断机制一般是在Minor GC之后触发的

  • 老年代空间分配担保机制
    年轻代每次Minor GC之后都会判断老年代的剩余可用空间,如果剩余的可用空间小于Eden区的所有的对象(包含垃圾对象)之和,就直接进行Full GC
    还可以配置一个参数:-XX:-HandlePromotionFailure(jdk8默认设置)配置了这个参数,就会去判断老年代的剩余可用空间是否大于之前每次Minor GC 进入老年代对象的平均大小,如果小于就直接Full GC ,如果还够用,那就执行Minor GC
    FullGC之后空间还是不够存放的话,就会报OOM异常(内存溢出)

流程图:
在这里插入图片描述

垃圾回收算法:

引用计数法:
给对象中添加一个引用计数器,每当有地方引用它,计数器就加1;当引用失效就减1;任何时候计数器为零的对象就是不可能再被使用。
这个方法实现简单,效率高,但是目前主流的虚拟机中都没有选择这个算法来管理内存,其主要原因是它很难解决对象直接相互循环引用的问题。例如下面代码
除了objA和objB相互引用之外,没有其他地方引用这两个对象,但是他们相互引用导致计数器不为0,就没法回收这两个垃圾对象

1	public class ReferenceCountingGc {
2 		Object instance = null;
3
4 	public static void main(String[] args) {
5 		ReferenceCountingGc objA = new ReferenceCountingGc();
6 		ReferenceCountingGc objB = new ReferenceCountingGc();
7 		objA.instance = objB;
8 		objB.instance = objA;
9 		objA = null;
10 		objB = null;
11 		}
12 }

可达性分析算法
将"GC Roots" 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

在这里插入图片描述
常见的引用类型

Java中的引用类型一般分为四种:强引用,软引用,弱引用,虚引用

  • 强引用:普通的变量引用

public static User user = new User();

  • 软引用:将对象用softReference软引用的对象包裹,正常情况下不会被回收,但是GC完以后还是释放不出空间存放新的对象,则会把这些软引用的对象回收点,软引用可用于实现内存敏感的高速缓存

public static SoftReference user = new SoftReference(new User());

  • 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用

public static WeakReference user = new WeakReference(new User());

  • 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值