本文为对《深入理解Java虚拟机》一书的简单总结,仅供个人学习使用。
JVM运行时数据区
Java虚拟机所管理的内存包括以下几个区域:
JVM运行时数据区下面分别对它们进行简单的介绍
- 程序计数器
Jvm将这个计数看作当前线程执行某条字节码的行数,会根据计数器的值来选取需要执行的操作语句。这个属于线程私有,不可共享,如果共享会导致计数混乱,无法准确的执行当前线程需要执行的语句。该区域不会出现任何OutOfMemoryError的情况。 - 虚拟机栈
经常说到的栈内存就是指虚拟机栈。Java中每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。 - 本地方法栈
虚拟机栈用来执行java方法,而本地方法栈用来执行本地(Native)方法。Java类的祖先类Object中有众多Native方法,如hashCode()、wait()等,他们的执行很多时候是借助于操作系统,但是JVM需要对他们做一些规范,来处理他们的执行过程。此区域,可以有不同的实现方法,如我们常用的Sun的JVM就是本地方法栈和JVM虚拟机栈是同一个。抛出异常的情况和虚拟机栈一样。 - 堆
是jvm中内存最大、线程共享的一块区域。唯一的目的是存储对象实例。这里也是垃圾收集器主要收集的区域。由于现代垃圾收集器都采用的是分代收集算法,所以java堆也分为新生代和老年代。
可以通过参数-Xmx(jvm最大可用内存)和-Xms(jvm初始内存)来调整堆内存,如果扩大至无法继续扩展时,会出现OutOfMemoryError的错误。 - 方法区
Jvm中内存共享的一片区域,用来存储类信息、常量、静态变量、class文件等数据。垃圾收集器也会对这部分区域进行回收,比如常量池的清理和类型的卸载,但是效果不理想。
虚拟机对象探秘
对象创建过程
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,加载检查之后会在堆中划分出一定的内存。
在完成new指令之后,紧接着会调用<init>方法将对象初始化,这时一个完整的对象才算创建了出来。
对象的访问
目前主流的访问方式有:使用句柄和直接指针两种方式。
通过句柄访问对象 通过直接指针访问对象使用句柄访问的最大好处是reference中存储稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,不需要更改reference本身。
使用直接指针访问方式的最大好处是速度更快,它节省了一次指针定位的时间开销。如果对象的访问在Java中非常频繁,将会节省比较可观的时间。
OutOfMemoryError错误
Java堆内存的OutOfMemoryError异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
当产生的对象过多时,会出现这个错误信息。解决办法:调整虚拟机堆参数(-Xmx和-Xms)。
Java方法区会存储类信息、常量、静态变量等。如果产生了大量类,比如某个ssh项目因为加载了框架和大量jar包,这样class文件都会载入内存的方法区,这样如果出现内存无法继续扩展的情况,也会出现java.lang.OutOfMemoryError,然后紧跟着PermGen space信息。通过-XX:PermSize和-XX:MaxPermSize可以限制方法区大小。
内存分配策略
垃圾回收(GC)需要完成的3件事情:
-
那些内存需要回收?
程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊的执行者出栈和入栈操作。每个栈帧中分配多少内存基本上在类结构确定下来时就已经确定,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题。
Java堆,方法区则不同,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分区需要的内存也可能不一样,只有在程序处于运行区才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,因此垃圾收集器所关注的是这部分内存。 -
什么时候回收?
在堆里面存放着Java中几乎所有的对象实例,垃圾收集器在对堆回收前,需要先确认这些对象中哪些还“存活”,哪些已经“死去”(即不能够被任何途径使用的对象)。
判断对象是否已经死亡,有如下方法
①引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就不能再被使用了。它很难解决对象之间相互循环引用的问题。
②可达性分析算法
通过一系列称为“GC Roots”的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
如上图,object5、object6、object7虽然相互有关联,但是它们到GC Roots是不可达的,所以它们被判定为可回收的对象。
在Java中,可作为GC Roots的对象包括:
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
- 如何回收?
当一个对象被标识为可回收对象时,还需要经历至少两次标记过程:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
如果这个对象被判定为有必要执行finalize()方法,那么它将会被放置在一个叫做F-Queue的队列之中,然后等待虚拟机使用Finalizer线程去执行它。如果对象在finalize()中成功拯救了自己--只要重新与引用链上的任何一个对象建立关联即可,否则,该对象就被真正的回收了。
垃圾收集算法
- 标记-清除算法
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。
主要不足:
①效率低下。标记和清除两个过程效率都不高。
②空间问题。产生了大量的不连续的内存碎片,导致以后产生较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
- 复制算法
解决效率问题,将可用内存按照容量分为大小相等的两块,每次只使用其中的一块。当一块内存用完时,将还存活的对象复制到另外一块上面,然后把已使用过的内存空间清理掉。
存在问题:内存缩小为原来的一半,成本较高。
- 标记-整理算法
“标记”过程仍然和“标记-清除”算法一样,但是后续步骤改为让所有存活的对象都向一端移动,然后直接清理端边界之外的内存。
分代收集算法
内存主要被分成三块:新生代,老生代,持久代
各内存区域图解-
新生代(New Generation或者Young Generation)
上面大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace 和ToSpace。新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代的大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例. -
老生代(Old Generation)
用于存放新生代中经过多次垃圾回收仍然存活的对象,例如缓存对象。老生代占用大小为-Xmx值减去-Xmn对应的值。 -
持久代(Permanent Generation)
在Sun的JVM中就是方法区的意思,尽管有些JVM大多没有这一代。主要存放常量及类的一些信息默认最小值为16MB,最大值为64MB,可通过-XX:PermSize及-XX:MaxPermSize来设置最小值和最大值。
在新生代中,每次垃圾收集都发现有大量对象死去,只有少量存活,选用复制算法。
在老生代中,由于对象存活率较高,没有额外的空间进行分配担保,使用 标记-清理算法 或 标记-整理算法。
网友评论