*作者:青芒@有赞
本文思路 1.JVM运行时内存划分 2.对象创建内存动作 3.Java线程栈 4.JVM垃圾收集器
1.JVM运行时内存划分
1.1 程序计数器
当前线程所执行的字节码的行号指示器。字节码解释器工作的时候就是通过改变这个计数值来选取下一条要执行的字节码指令。
1.2 虚拟机栈(java方法栈)
线程私有,java方法执行时的内存模型,每个方法执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、方法出口等信息
局部变量表其实就是一个方法内的局部变量对应的指针。
1.3 本地方法栈
Native 方法执行的线程栈
1.4 方法区
线程共享区域,存储已经被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。
运行时常量池 方法区的一块,保存Class文件中描述的符号引用,也会把翻译出来的直接引用也存储在这里。它的主要特征是具备动态性,编译器和运行时的常量都可以进入,比如的string 字符串常量池 intern()方法,就是利用的该特性。
1.5 堆
JVM里所管理的内存中最大的一块,线程共享,存放所有对象实例。GC也是针对该区进行回收。
2.对象创建内存动作
我们new一个对象的时候,都发生了什么?
2.1 jvm看到new指令,判断指令参数是否能在常量池中定位到这个类的符号引用(可以认为我们的类名),检查这个符号引用代表的类是否已被加载、解析和初始化过,没有的话需要进行class加载。
2.2 jvm从堆里分配内存
2.3 分配的内存空间都初始化为零,保证对象的实例字段再Java代码中不赋初始值就能直接使用。
2.4 jvm对对象做一些设置。设置存放在对象头,对象头里主要包含一些hashcode、GC、元数据、锁等。
2.5 执行init方法。此刻对于JVM一个对象已经创建完毕,接下来会按照程序员的一样做一些初始化动作。
3.Java线程栈
每执行一个方法,jvm都会为此创建一个栈帧,我们的一条调用链路,有一个一个的栈帧组成,最后形成一个线程栈,当栈的长度过长或者超过内存大小就好报栈溢出的异常。
基本数据类型,string这些的指针指向的是常量池。而实例对象也就统称为reference类型,指向的是一个对象的引用,访问堆中的对象具体位置有两种方式,通过句柄池进行句柄访问,另一种直接通过指针。
3.1 句柄方式
java堆中会划分一块内存用来作为句柄池,reference存储的就是句柄地址,而句柄包含了实例数据和类型数据各自的具体地址。具有稳定的句柄地址的优势,当对象移动,只会更改句柄中的实例数据指针。
3.2 直接指针访问
reference存储的直接就是对象地址,速度快,节省了一次指针定位的开销。java中对象的访问特别多,所以在java中该方式使用的最多。
4.JVM垃圾收集器
栈,程序计数器这些随线程而生,随线程而忘。而Java堆和方法区则是在运行期间才知道会创建哪些对象,需要多少内存。垃圾回收器所关注的则是这部分内存。
如何判断对象对象已死? 目前主流实现主要基于可达性分析来判定对象存活。大概思路就是通过GC ROOT 对象向下分析,不在链路上的对象则认为是已死对象。
哪些对象可以作为GC ROOT对象?
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈中引用的对象
安全区域 JVM可以通过GC ROOT快速定位到要回收的对象,但是程序是在运行中的,对象的关系也会发生变化。JVM只有当程序进入安全点时,然后暂停程序进行回收。
安全点的选定是以程序 是否具有长时间执行的特征
为标准进行选定。“长时间执行”最明显的特征就是指令序列复用(一组),如方法跳转、循环跳转、异常跳转等,具备这些功能的指令才会产生safePoint。
在GC发生时,一般会采用主动式中断,jvm在要发生GC时不会干预线程,会设置一个标志,所有的线程主动去轮询,当执行线程发现了这个标识就自己中断挂起。
回收算法 标记清除 步骤:将内存中需要回收的对象打上标记,然后回收打上标记的对象。 缺点:会产生内存碎片
复制 步骤:将内存一分为2,需要回收的时候,将存活对象copy到另一个内存区域,然后原来的内存区域就只剩下待回收的对象,直接全部清除 缺点:可使用内存只有一半,代价太大
标记整理 和
标记清除
类似,多了一个整理的过程,在清除后,会让存活的对象向一个方向移动,最后达到内存连续,解决碎片问题。
收集器
JVM采用分代垃圾回收。在JVM的内存空间中把堆空间分为年老代和年轻代。将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。年轻代中又被分为Eden区(圣经中的伊甸园)、和两个Survivor区。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次收集仍然存活的,就会被转移到年老区。
JVM提供了很多个收集器,目前主要有Serial,Serial Old,ParNew, Paralle Scavenge,Paralle Scavenge Old,CMS,G1。当前用的最多的是 CMS和G1。 Serial 和 SerialOld 单线程,简单高效。进行GC的时候会把所有工作线程停掉,是早期的回收期。serial old是专门回收老年代的。
ParNew Serial的并发版本
ParalleScavenge 和 ParalleScavengeOld 采用复制算法,追求吞吐量,可以高效的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的程序。
$$吞吐量 = \frac{运行用户代码时间}{运行用户代码时间 + 垃圾收集时间} \quad$$
CMS 是一种以获取最短回收停顿时间为目标的收集器。对于尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验的系统很适合。
采用标记清除算法,所以缺点就是会有大量的空间碎片。
G1
-- 参考文章
http://www.idouba.net/a-simple-example-demo-jvm-allocation-and-gc/
http://gityuan.com/2016/01/09/java-memory/
长按二维码,扫扫关注哦
✬如果你喜欢这篇文章,欢迎分享和点赞✬