身为一个java程序员如果只会使用而不知原理称其为初级java程序员,知晓原理而升中级、融会贯通则为高级
作为有一个有技术追求的人,应当利用业余时间及零碎时间了解原理
近期在看深入理解java虚拟机 第二版(基于jdk1.7)所以想写一些观后心得,整理一些比较重点的内容,也加强自己对重点内容的记忆!
以下默认虚拟机为hotsport虚拟机
一.jvm内存模型
① 程序计数器:
程序计数器为每个线程私有
作用可以理解为在虚拟机解析字节码时记录当前字节码的行号
占用的内存较小
是唯一一个不会发生内存溢出(OOM)的区域
②虚拟机栈:
虚拟机栈也是线程私有的
虚拟机栈包括了java虚拟机栈以及本地方法栈,两者唯一的区别是前者是针对java后者是针对本地方法(本地方法可以理解为操作系统的方法)
栈中的一个栈帧包含了一个方法中的局部变量、方法出口、操作数栈、动态链接等各种信息
需要注意栈中存储的只有基本数据类型以及引用类型变量,对象的内存分配都在堆中
每个栈帧的入栈到出栈对应着线程调用一个方法到方法执行结束
栈的深度是动态扩展的
当线程调用深度大于虚拟机允许的深度时(使用xss参数调整虚拟机栈内存容量)会发生StackOverflowError(SOF)异常
栈帧的每次入栈会判断栈的内存容量是否足够,发现内存不足即会申请内存,当无法申请到足够的内存时会发生OutOfMemoryError(OOM)异常
因为虚拟机栈严格遵守方法调用 入栈 方法执行结束 出栈 的规则,所以无需gc回收
③堆
java堆为gc(Garbage Collecte11d) 回收的主要区域
java堆被所有线程共享
java堆存放着几乎所有的对象实例,它也只存放着对象实例
堆中分为年轻代,年老代
年轻代又被细分为Eden区和两个survivor区,该区的gc回收算法为复制算法,关于gc算法下文会讲到
年轻代存储的都是新生不久的对象(未经过gc回收或者经过不多的gc回收,使用MaxTenuringThreshold参数设置经过几次gc后对象可以晋升到年老代)
年老代存储的是一些占用内存较大的大对象(使用PertenureSizeThreshold参数设定这个内存界限值)或者经过数次gc后还存活的对象
还有一种情况,当Survivor区中同一年龄的所有对象大小总和大于Survivor区总内存的一半时,将大于等于该年龄的对象全部晋升年老代
年老代因为存储大对象以及对象存活周期较长的特性,所以采用标记-整理算法
当有对象实例化到堆中时,如果堆中无法提供连续的内存空间就会触发gc,而gc后仍无法分配足够的内存空间就会发生OOM异常
④方法区
方法区被所有线程共享
方法区存储着已加载的类信息、类的静态变量、常量等数据
方法区包含了运行时常量池,其中包括了编译期生成的各种字面量和符号引用,并且运行时常量池可以在进程运行时动态增加数据
方法区有些时候也和年轻代和老年代并称为永久代,但是其实方法区并不永久,方法区也有gc回收
当常量池中的对象不存在任何引用时将被gc回收
当一个类不存在任何实例、加载该类的classLoader 已经被回收、对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法,才能被回收
同样的当方法区无法满足内存分配需求时会发生OOM异常
二.gc策略以及gc算法
在虚拟机栈以及程序计数器中因为栈帧入栈出栈的稳定性所以不需要过多考虑gc问题,这里主要对java堆中内存gc做讲解
我们只需要明确三点就可以理解gc的过程
1.什么内存需要被回收
2.什么时候回收
3.怎样回收
什么内存需要被回收:当然是已经不需要再用到的对象才需要被回收,如何判断这个对象已经无用?这里有两种判断算法一是引用计数算法,二是可达性分析算法,引用计算算法就是计算各个对象之间的引用关系数量java采用的是后者,所以前者这里不多做介绍
可达性分析算法:使用一系列对象作为gc roots 也就是根节点遍历整个引用链,在引用链之外的对象既无用对象
可以作为gc roots的对象如下:
java虚拟机栈中引用的对象
方法区中静态变量引用的对象
方法区中常量引用的对象
本地虚拟机栈中引用的对象
这里需要注意引用这个概念,引用分为四种引用,四种引用排序从强至弱如下:
强引用——所有New出来的对象都为强引用,引用链上的对象只要存在强引用即使发生OOM异常也无法被gc回收
软引用——当系统即将发生内存溢出时,会优先回收掉软引用
弱引用——当发生gc时直接回收弱引用
虚引用——没有任何作用,虚引用甚至不能获取到对象实例,唯一作用是在对象被回收时会收到一个系统通知
什么时候回收:大多数情况下对象都会分配在Eden区,而当Eden区内存不足时就会触发Minor Gc
所以分配率越高,越频繁执行 Minor GC,Minor Gc 可以理解为只对年轻代进行Gc回收的策略。
当年轻代Survivor空间不足,并且年老代所剩空间也无法为Surivivor承担时就会发生Full Gc 可以理解为对全部的堆进行Gc回收
在大对象分配时老年代空间不足发生Full Gc
在方法区进行类的装载或者常量池动态增加常量时方法区空间不足 发生Full Gc
怎样回收:
常见的三种gc算法:标记-清除、复制、标记整理
标记-清除——先标记所有可用对象,再清除掉所有非可用对象
复 制——划分出A、B两片内存空间,将所有数据 存在A区域中,当发生gc时将可用对象复制到B空间,然后一次清除A空间中所有内存,下一次gc则是重复B复制到A 再清除B空间内存
标记-整理——标记所有可用对象,再将所有可用对象移动到一端,再一次性清除掉端界外的非可用对象
结合年轻代以及年老代的性质分析各个区域的gc算法
年 轻 代——因为年轻代Eden区对象绝大部分都是 “朝生夕死” 所以使用标记-清除或者标记-整理效率会很低,而使用效率高的复制算法又非常浪费空间,所以年轻代改进了复制算法,使用Eden区和两块Survivor区,因为Eden区一次gc存活下来的对象通常非常少,所以年轻代默认采用8:1:1的比例对Eden区和两块Survivor区进行内存分配,当发生gc时 将Eden区存活的对象和 一块Survivor区中存活的对象 复制到 另一块Survivor区中,然后清除掉Eden 区和第一块 Survivor区的内存,这样既保证了gc效率,又不至于会浪费大量的内存空间
年 老 代——年老代因为存放大对象或者生命周期较长的对象,所以不能像年轻代一样使用复制算法,因为那样可能会导致大量的对象复制,导致效率极低,而这个时候标记-整理算法的优势体现出来,它只需要标记出 在年老代 为数不多的 非可用对象然后将非可用对象移动到堆的末端后一次性清除即可