首页 > 代码库 > Java内存组成和垃圾回收机制
Java内存组成和垃圾回收机制
眼看就要到找工作的时候了,平时在实验室也做了不少项目,可到头来,假设面试官问我平时做过什么,我确不知从何说起,也可以说我不知道说什么。前辈们早就说过,计算机这个行业需要不断的学习,也需要不断的积累,自问平时遇到过不少问题,也解决了不少问题,可到头来,好像都没什么印象了!在准备找工作的时候,就将平时一些研究过的,倒腾过的重新记录下吧!由于本人是第一次写博客,文笔不太好,内容可能也有很多借鉴了是前辈们的,但重在重新整理、精选,也让自己在整理的过程中重新学习,加深印象!
一、内存组成
在我做项目的时候,tomcat会经常报"java.lang.OutOfmemoryError: PermGen Space"的错误,网上的解决办法很多,我一般也就按照这些方法改下tomcat的配置就ok了!前段时间,帮老师去北京部署项目,在一个tomcat下面部署了三个过程。访问前两个工程时一切ok,但在访问第三个工程时,就出现了同样的错误。这次我就把这个问题深入研究了下。
了解这个问题的本质,就需要知道JVM的内存组成。打开JDK自带的Jconsole.exe,可以看到目前JVM的运行情况:
可以看出,JVM主要管理两种类型的内存:堆和非堆。借用一张图,可以看得更清楚一些:
1. 方法区
方法区与Java堆栈一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等等。它是一个非堆【Non-Heap】。
在HotSpot虚拟机中,很多人都把方法区成为永久代,其实只在hotspot才存在方法区,在其他的虚拟机没有方法区这一个说法的。之前上面提到的tomcat报"java.lang.OutOfmemoryError: PermGen Space"错误就是该区域已经满了。之所有在访问第三个项目时才报错,是因为在第一次访问该项目时,才将该项目的相关Class文件装载到JVM的方法区中。
2. 程序计数器
程序计数器是一个很小的内存空间,它的作用其实很简单,就是当前线程所执行的字节码行号的指示器。在Hotspot虚拟机中字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理等等。
3. Java虚拟机栈
Java虚拟机栈和程序计数器一样的,都是线程私有的。在Java中,JVM中的栈记录了线程的方法调用。在某个线程的运行过程中,如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即帧(frame)。在frame中,保存有该方法调用的参数、局部变量和返回地址。
4. 本地方法栈
本地方法栈和虚拟机栈差不多,只不过Java虚拟机执行java的方法,本地方法区执行的是native方法而已,如JVM加载jre中类时调用的Bootstrap ClassLoader,其由C++实现。
5. 堆(存放对象的属性,给每个属性分配内存空间)
在Java程序员中堆,肯定不陌生,堆是使用的最多的,也是程序猿最关心的一块内存区域。堆中所有的线程都共享一个内存区域,在虚拟机启动的时候就被创建,此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配。
二、垃圾回收机制(GC)
说到内存分配,就不得不说内存的回收了。熟悉C++编程的应该知道,在编程中,我们需要手动的释放堆中的对象,如果忘记了,则那块区域将永远不会释放,一不小心则会发生内存泄露,一般只能重启机器了!而Java提供提供的垃圾回收机制(GC)可以自动清空堆中不再使用的对象!这个功能是相当nice的,但是另一方面,垃圾回收需要耗费更多的计算时间,这也是Java比C++慢一个原因吧。对于Java的GC,个人觉得@Vamei前辈写的还不错,在此借鉴下:
在Java中,对象的是通过引用使用的(把对象相像成致命的毒物,引用就像是用于提取毒物的镊子)。如果不再有引用指向对象,那么我们就再也无从调用或者处理该对象。这样的对象将不可到达(unreachable)。垃圾回收用于释放不可到达对象所占据的内存。这是垃圾回收的基本原则。(不可到达对象是死对象,是垃圾回收所要回收的垃圾)
早期的垃圾回收采用引用计数(reference counting)的机制。每个对象包含一个计数器。当有新的指向该对象的引用时,计数器加1。当引用移除时,计数器减1。当计数器为0时,认为该对象可以进行垃圾回收。
然而,一个可能的问题是,如果有两个对象循环引用(cyclic reference),比如两个对象互相引用,而且此时没有其它(指向A或者指向B)的引用,我们实际上根本无法通过引用到达这两个对象。因此,我们以栈和static数据为根(root),从根出发,跟随所有的引用,就可以找到所有的可到达对象。也就是说,一个可到达对象,一定被根引用,或者被其他可到达对象引用。
橙色可到达,绿色不可到达
JVM的垃圾回收是多种机制的混合。JVM会根据程序运行状况,自行决定采用哪种垃圾回收。可看下Jconsole的监测情况:
我们先来了解"mark and sweep"。这种机制下,每个对象将有标记信息,用于表示该对象是否可到达。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到所有的可到达对象,并标记(mark)。随后,JVM需要扫描整个堆,找到剩余的对象,并清空这些对象所占据的内存。
另一种是"copy and sweep"。这种机制下,堆被分为两个区域。对象总存活于两个区域中的一个。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到可到达对象,将可到达对象复制到空白区域中并紧密排列,修改由于对象移动所造成的引用地址的变化。最后,直接清空对象原先存活的整个区域,使其成为新的空白区域。
可以看到,"copy and sweep"需要更加复杂的操作,但也让对象可以紧密排列,避免"mark and sweep"中可能出现的空隙。在新建对象时,"copy and sweep"可以提供大块的连续空间。因此,如果对象都比较"长寿",那么适用于"mark and sweep"。如果对象的"新陈代谢"比较活跃,那么适用于"copy and sweep"。
上面两种机制是通过分代回收(generational collection)混合在一起的。每个对象记录有它的世代(generation)信息。所谓的世代,是指该对象所经历的垃圾回收的次数。世代越久远的对象,在内存中存活的时间越久。
根据对Java程序的统计观察,世代越久的对象,越不可能被垃圾回收(富人越富,穷人越穷)。因此,当我们在垃圾回收时,要更多关注那些年轻的对象。现在,具体看一下JVM中的堆:
我们看到,堆分为三代。其中的永久世代(permanent generation)中存活的是Class对象。这些对象不会被垃圾回收。年轻世代(young generation)和成熟世代(tenured generation)需要进行垃圾回收。年轻世代中的对象世代较近,而成熟世代中的对象世代较久。
世代
年轻世代进一步分为三个区域
- eden(伊甸): 新生对象存活于该区域。新生对象指从上次GC后新建的对象。
新生对象生活于伊甸园
- from, to: 这两个区域大小相等,相当于copy and sweep中的两个区域。
当新建对象无法放入eden区时,将出发minor collection。JVM采用copy and sweep的策略,将eden区与from区的可到达对象复制到to区。经过一次垃圾回收,eden区和from区清空,to区中则紧密的存放着存活对象。随后,from区成为新的to区, to区成为新的from区。
如果进行minor collection的时候,发现to区放不下,则将部分对象放入成熟世代。另一方面,即使to区没有满,JVM依然会移动世代足够久远的对象到成熟世代。
如果成熟世代放满对象,无法移入新的对象,那么将触发major collection。JVM采用mark and sweep的策略,对成熟世代进行垃圾回收。
第一次写博客还是借鉴的比较多哈,呵呵,不过自己写了才有收获!大神们说过,不管多还是少,不管简单还是难,觉得有意义的都可以记下来,重在积累,重在分享!
Java内存组成和垃圾回收机制