关于理解Java虚拟机总结 JVM高级特性与最佳实践 jvm自动内存分配

笔者最近在总结关于JVM的知识,此篇文章建立在 周志明编写的 《深入理解Java虚拟机 JVM高级特性与最佳实践》第三版的基础之上进行总结。总结如果不正之处,还望多多指教。

关于对Java虚拟机的介绍以及虚拟机的发展史在这里就不在赘述,如果感兴趣可以看一些资料。

Java内存区域

学过c++的开发人员会知道,c++在内存管理领域是扮演者“皇帝”的角色,他们负责着一个对象从出生到终结的整个过程。然而对于Java程序员来说,他们只负责创建对象,而不用去释放这些对象所消耗的内存,这些工作全部交由jvm去做。

运行时数据区域

在这里插入图片描述
虚拟机将内存分为了若干个不同的区域,每块区域负责着不同的事情。
其中如上图所示,白色区域是线程私有的,灰色区域是所有线程共有的。

程序计数器:

 它是一块较小的内存空间,程序计数器记录着当前线程所执行语句的行号。也就是说通过它线程可以知道当前所处的位置,
 每个线程都有自己的程序计数器,互不影响,这样在线程切换时就不会造成代码执行错误的问题。

Java虚拟机栈:

他是描述Java方法执行的线程内存模型。每个方法在执行的时候都会创建一个栈帧,用来储存局部变量表、操作数栈、动态连接、方法出口等。
每一个方法执行到结束都会对应一个栈帧入栈和出栈。我们平时所说的栈更多的是指这里的局部变量表部分。它存储着基本类型数据、对象引用和returnAddress类型。
他的大小实在编译器确定的,在方法执行过程中不会改变大小。

本地方法栈:

本地方法栈和虚拟机栈非常相似,区别在于本地方法栈为执行本地方法(Native)服务。

Java堆:

Java堆是内存中最大的一块区域,是线程共享的,几乎所有的对象实例都在这里存储。也是被垃圾收集器管理的一块区域。物理上不连续,但是逻辑上是连续的。

方法区:

它存储着被类加载器加载的类型信息、常量、静态变量、以及即使编译器编译后的代码缓存等,它是线程共有的一块区域。
虚拟机对方法区非常宽松,方法区甚至是可以不实现垃圾收集,因为它的回收价值比较低。

运行时常量池:

运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。它具有动态特性,运行在运行时将新的常量加入。

直接内存:

它并不是虚拟机运行时数据的一部分。在JDK1.4中新加入NIO类,它可以使用本地方法直接分配堆外内存。很显然他并不受堆大小的限制。

HotSpot虚拟机对象探索

1、对象的创建

当new一个新对象时,首先去检查这个参数能否在常量池中定位到一个类的符号引用,并检查这个类是否以及被加载、解析和初始化过,如果没有那先执行这些过程。然后在为对象分配内存,类的大小的类加载的过程就已经确定,在堆中划分出一块这样大小的内存(两种方式:指针碰撞和空闲列表)。

并发情况下分配内存并不是安全的,有俩种方式可供选择:一是将分配空间的动作进行同步处理,保障操作的原子性;
另一种是为每个线程提前分配一块各自的内存(本地线程分配缓冲区)。

接下来还要将该对象的一些信息如:对象的哈希码、是哪个类的实例、如何找到元数据信息、对象的GC分代年龄等保存在对象头中。
最后是对对象的初始化操作,这样一个对象才被完全构造出来。

2、对象的内存布局

对象在堆中的内存布局分为三个部分:对象头、实例数据、对齐填空。

对象头包含两部分信息:一是其自身的运行时数据(锁状态标志、偏向线程id、偏向时间戳、分代年龄等),长度在不同位数的虚拟机上有所不同,32位虚拟机中占32比特、64位虚拟机中占64比特;另一种是类型指针,即对象指向它的类型元数据指针。
**如果对象是一个数组,那么它的对象头中还包含记录数组长度的数据。

实例数据是对象正在存储的有效信息。

对齐填空它仅仅起到占位符的作用,虚拟机要求任何对象的大小必须的8字节的整数倍,它用来补全对齐。

3、对象的访问定位

Java程序会通过栈上的reference来访问堆上的具体对象,有俩种方式区访问:句柄和直接指针。

使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,如下图句柄形式结构图。
句柄

使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址。下图是直接指针形式结构图。

直接指针
使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销。

垃圾收集器与内存分配策略

对象已死?

如何判断一个对象是否需要被垃圾回收器收集。

引用计数算法

思想很简,在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
但是这样会留下一些问题,当俩个对象互相引用,且没有其他引用时,这个对象应该是被收回的,但是由于他们相互引用,此时计数器并不为0,所以无法对他们进行收回。

可达性分析算法

这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在这里插入图片描述
可以固定作为GC Roots的对象:

	* 虚拟机栈中的引用对象
	* 方法区中静态属性引用的对象
	* 方法区常量引用的对象
	* 本地方法栈中JNI引用的对象
	* 虚拟机内部的引用
	* 同步锁持有的对象
	* 等等

在谈引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Objectobj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

生存还是死亡

要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

为任何一个对象的finalize()方法都只会被系统自动调用一次。

回收方法区

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

·该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
·加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方
法。

垃圾收集算法

(1) 标记-清除算法

如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程,这在前一节讲述垃圾对象标记判定算法时其实已经介绍过了。

  • 执行效率不稳定、
  • 内存空间碎片化

(2)标记-复制算法

为了解决标记-清除算法面对大量可回收对象时执行效率低的问题而提出的算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

  • 实现简单,运行高效
  • 空间利用率比较低
  • 没有碎片

Andrew Appel针对具备“朝生夕灭”特点的对象。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。这些对象便将通过分配担保机制直接进入老年代

(3)标记-整理算法

针对于老年代的特点创建了这样的算法。
其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

  • 移动对象带来的开销很大
  • 内存是规整的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值