jvm内存模型,及垃圾回收细节

首先是jvm规范。在规范里jvm的内存模型,分为堆、栈、方法区。

在jdk1.7中jvm的实现只有堆内存、栈内存。堆内存又分年轻代、年老代、永久代。这里的永久代对应规范中的方法区。也就是在1.7的实现里,方法区是堆内存的一部分。

在jdk1.8中jvm的实现由堆、栈、元空间。元空间对应规范中的方法区。在1.8中,方法区是一块不同于堆内存的独立内存空间。

栈是线程运行储存变量的地方。堆是储存对象的地方。一般所说的jvm内存特指的jvm堆内存。

jdk1.7堆内存分年轻代、年老代、永久代。之所以这么分,是为了提高内存回收的效率。永久代对应规范的方法区。主要储存类信息、常量等。这些信息几乎是不变的。所以永久代的内存空间占用几乎不变,也几乎不参与内存回收。

我们新建一个对象,首先是放到年轻代中。我们新建的绝大部分对象都是朝生暮死。但有一小部分对象,存活时间长。那么这个存活时间长的对象,就会被放到年老代中。

为了提高内存回收的效率,年轻代和年老代采用不同的内存回收方式。年轻代采用复制法来回收内存。而年老代,采用移动法来回收内存。

我们对年轻代进行内存回收,95%的对象都是要删除。这个时候我们只需要把活下来的对象复制到一小块干净的内存空间里,对其余空间进行格式化就行。

而老年代,内存回收不一样。一次内存回收可能有超过50%的对象是要存活的,仍然采用复制法,效率太低。于是呢,就采用移动法。把存活的对象,移动,挨在一起。把由于对象死亡造成的不连续的小片内存,连在一起,形成一块大的可用内存。

年老代内存回收示意图

jvm为连续内存空间的大对象进行内存分配是一个噩梦。比如一个很长的字符串。连续大内存的对象gc开销会很大。为了减少对此对象的gc操作,jvm会直接将其放入年老代。jvm内存由于使用,内存并不连续。无法找到一块可回收的内存碎片足够放下此对象的话。就会触发一次full gc,而此时jvm内存的使用百分比也许并不高。full gc不会并发执行。会把所有的线程挂起,直到内存回收结束(年轻代的内存回收,已经有可以并发执行的垃圾回收器了,full gc没有)。

比较严重情况会直接抛出内存溢出的异常(这个坑我踩过,10M左右的字符吧)。

年轻代的内存空间实际上也分为三个部分。10%、10%、80%。jvm会首先把对象分配一个10%和80%中。内存回收的时候把活下来的对象复制到另一个10%中,再格式化其余内存空间。因为年轻代超过90%的对象都会被回收,所以这样复制内存回收,只复制不到10%的对象。比移动法内存回收性能高的多。当然也有可能出现,10%内存不够的情况。这时,年轻代就会向年老代借内存,gc完成后归还。如果年老代没有足够内存出借的话。就会触发full gc。full gc后还是内存不够,那么会抛出内存溢出异常。

因为年轻代gc可能会向年老代借内存。所以在设置内存参数的时候年老代内存设置的太小,那么设置的参数就会失效。

比如我给年轻代设置500M,给年老代设置100M。那么虚拟机运行起来后你会发现。年轻代内存肯定小于400M,你的设置参数是失效的。年老代内存,好像不会低于年轻代内存的2/3。

栈是jvm运行时保存方法变量的地方。线程运行一个方法时会起一个栈帧,方法运行结束,栈帧销毁内存回收。栈不会出现gc的问题。需要注意的是,每个线程,运行每个方法,都会起一个栈帧。也就是说。两个线程运行同一个方法,是完全不同的两个栈帧。所需要的变量也是各个栈帧各一份。互相不可见。

如果运行的方法里有成员变量。在各个栈帧里都会复制一份这个成员变量。各个栈帧又都不可见。就会出现内存可见性的问题。也就是线程安全问题。

并发的时候。线程1 拿到成员复制成员变量a的值100,此时线程2页复制了成员变量a的值100,线程1对值进行减1变成99再同步回主内存中的成员变量a。线程2也减1变成99再同步回主内存中的变量a。a的值最终是99,被少减了一次。

我们的线程锁就是在用到这个成员变量时,都要去主内存中同步一次。保证拿到的是最新值。解决内存可见性的问题。

乐观锁volatile就是每次使用到这个关键词修饰的变量都要和主内存中的变量同步一次。解决了可见性,却没有解决原子性。

悲观锁synchronized也是每次每次用到加锁的变量要去主内存中同步一次,不同的是,还会给变量加锁。直到大括号里的代码执行完毕后解锁。这样,既保证了可见性,又保证了,原子性。

对java虚拟机内存模型,及内存回收机制,做一次总结。

声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!

(0)
上一篇 2017年12月13日
下一篇 2017年12月13日

相关推荐