55

Java虚拟机的Heap监狱

 5 years ago
source link: http://www.10tiao.com/html/332/201806/2652434751/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

来自:码农翻身(微信号:coderising)


在Java虚拟机中,我是一个位高权重的大管家,他们都很怕我,尤其是那些Java 对象,我把他们圈到一个叫做Heap的“监狱”里,严格管理,生杀大权尽在掌握。


中国人把Stack翻译成“栈”,把Heap翻译成“堆”, 还有人会把Stack翻译成“堆栈”,唉,真不知道他们是怎么想的, 不过这么多年都过来了,你们明白就好。


碰巧我会对Heap中的Java 对象做垃圾回收,这个“堆”总是让我联想到垃圾堆。


说起垃圾回收,这实在是一个大负担,原因很简单,那些写Java程序的人类只管把对象给new出来,扔到Heap 中, 但是从来不管把他delete 掉, 删掉这些对象的责任就落到了我的头上,我不严格管理怎么行?


有时候我挺羡慕C和C++, 必须得手动地分配和释放内存,出了错都是程序员来背锅。


在我这里,如果任由这些对象对象肆意妄为,我那容量不高的,Java虚拟机启动后就无法更改的Heap“监狱”很快就会被填满, 所以我必须得派出我的得力助手,专门找到并且清理那些不用的Java 对象, 把他们占据的空间给释放掉。


为了找到这些捣乱分子,我发明了一个叫做“可达性分析”的算法,这个算法估计大部分人已经知道了,我就不再啰嗦了,下面这张图说明了背后的思想,聪明的你一眼就能看出来, 橙色的对象都是不可达对象,可以回收。



有个叫刘欣的家伙写了一篇文章《我是一个Java Class》 ,发表在码农翻身公众号上,文章说我还在用引用计数这种办法,实在是可笑。 我抗议了很多次,让他修改, 他说微信公众号只能改五个字,改不了,唉,真是没辙。


Heap监狱


好吧,现在详细说一下我管理的Heap“监狱”。


你可以把它想象成一大片空间,为了方便管理, 我把Heap“监狱”划分成多个区域,然后把那些Java对象在其中搬来搬去。


我定的规矩就是: 新来的家伙们都要进入新生代待着,新生代住不下了,我就派出清理者进行垃圾回收(Minor GC),回收以后还住不下,那就把年龄大的老家伙们赶到养老院(老年代)去。


每个在Heap中的Java对象我都会设置一个年龄计数器,每次Java对象熬过一次GC,就把年龄加1, 如果老到一定程度,对不起,请进入养老院(老年代)。  实际上我还会做动态的年龄判断,这里按下不表。


你可能会觉得奇怪,为什么在新生代里分出了Eden, Survivor1, Survivor2这样奇怪的区域?


那是因为我想在这里实现一个所谓的“复制”算法。


最早的时候, 我是把一个内存的区域划分成大小相当的两个区域,每次只用其中的一个。



区域1用完了,我就做垃圾回收,把存活的都搬到另外一个区域。


注意:搬过去以后,他们都会紧紧地挨在一起居住,这样以来,被清理掉的那些红色碎片就会重新平整成一大块空间,方便后续使用,尤其是针对大块头对象来了以后。


这么来回颠倒着使用两个区域,虽然效率高,没有碎片,但是浪费的空间很巨大:每次只能用一半。


后来人类发现,大部分在新生代的对象都活不了多长时间,基本上一次垃圾回收就删除得差不多了。


所以就改进了这个只用一半的复制算法, 把新生代分成三个部分:Eden , Survivor1, Survivor2 , 他们的比例是8:1:1。


每次只使用Eden 和其中一个Survivor , 当垃圾回收时,把这两块区域中还活着的对象复制到另外一个Survivor, 如果Survivor放不下,请进养老院(老年代)吧。


如果很不幸, 连养老院都住满了,那只好搞一次Full GC了,这是个很慢的操作,你们最好祈祷它不要频繁发生。


“监狱”之外,大有可为


虽然我可以在Heap监狱内作威作福,有时候我也得接触下监狱之外的世界。


有一次要通过Socket向外发送数据,我明明把数据准备好了,就在我的Heap中,可是JVM老大竟然把数据复制了一份到Heap之外的内存中去,然后才能通过Socket发送。


我问他这到底是怎么回事,为什么要多此一举,难道是对我这个Heap监狱的大管家不放心?


JVM老大说确实是不放心,人家底层的Socket都是C语言写的, 关注的是物理内存的地址, 你垃圾回收的时候把Java对象在什么Eden, Survivor, 老年代之间挪来挪去,对象的地址也会变来变去, 我怎么告诉人家到底发哪个地址的数据啊?


想想也是这个理儿,有得必有失,你程序员不用管理内存,但是底层还得和内存打交道,并且还额外多了一道工序:Copy 。


老大还说:“可能你还不知道,除了你的Heap监狱,其实我在Java进程中还有一块儿叫做“Off-Heap内存’的地方,数据就会复制到这里。 为了和你区分开,我把它叫做堆外内存。”


没想到这里还有一块我都管不着的“飞地”!


不过它和我也没有什么竞争关系,由它去吧。


可是没过几天,JVM老大再次给我带来了“惊喜”。


他说:“复制数据太麻烦了,我想了个办法,可以在Java代码中直接分配一块属于Off-Heap的内存。”


我觉得头皮发蒙:“直接在堆外内存分配?到底怎么分配?”


老大给了我一段代码:“看看,这不就分配了128M的堆外存吗? 对这个buffer的读写操作会直接写入堆外内存, 不用再经过你来复制了。”


ByteBuffer buffer = ByteBuffer.allocateDirect(1024*1024*128);


该死的面向接口编程,这个ByteBuffer分配出来的堆外内存,就像一个普通的Java对象在使用,丝毫看不出它在堆内还是在堆外。


完了,这块内存我是彻底管不了了。


老大看出我情绪不对,安慰道: “这个buffer也是个Java对象啊, 就在你的Heap中存着,只不过它保存了那128M内存的信息而已。”


这还差不多 ! 既然它是个Java对象,那就得放到我的Heap监狱中,被我控制!


可以想象,这个对象被垃圾回收的时候, 它指向的直接内存才会被释放。


我突然有了一个邪恶的想法:如果这样的对象越来越多,并且一直不被垃圾回收,那对应的直接内存岂不也是不能释放,然后Out of Memory ?


老大似乎看穿了我的思想:“对于这些对象,得特别小心,一定得确保能释放。”


直接分配堆外内存的功能正式推出了,我发现分配起堆外内存要比堆内内存要慢一点,心想估计没有多少人使用吧。 可没想到的是它特别适合那些分配次数少,读写操作很频繁的场景。于是就受到了Netty这些通信类系统的热烈欢迎。


为了减少创建堆外内存的开销,Netty 还引入了对象池的技术,就像数据库连接池一样,先分配一些堆外内存, 然后不断地复用他们。


我没想到堆外内存能玩出这么多的花样,但是一想到他们还是Java程序,还得用Java对象包装,无论如何都跳不出我的手掌去,也就释然了。


(完)



●编号715,输入编号直达本文

●输入m获取文章目录

推荐↓↓↓  

Python编程

更多推荐:18个技术类微信公众号

涵盖:程序人生、算法与数据结构、黑客技术与网络安全、大数据技术、前端开发、Java、Python、Web开发、安卓开发、iOS开发、C/C++、.NET、Linux、数据库、运维等。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK