每天一篇系列:
强化知识体系,查漏补缺。
欢迎指正,共同学习!
JVM内存区域
由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:
程序计数器(Program Counter Register)
Java栈(VM Stack)
本地方法栈(Native Method Stack)
方法区(Method Area)
Java堆(Heap)
1.程序计数器。
每个线程所私有
2.JAVA栈
每条线程都是一个栈,每个方法在执行的时候会创建一个栈帧,存储了局部变量表,操作数栈,动态连接,方法返回地址等
3.JAVA堆
被所有线程共享的一块内存区域,在虚拟机启动时创建,在JVM中只有一个堆,用于存放对象实例,对象实例包含了成员变量和成员方法。
4.方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域,在方法区中,存储了已被虚拟机加载的类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
5.本地方法栈
在本地方法栈中执行非java语言编写的代码
JVM内存模型
用户磁盘是数据存储的主要区域,CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用于缓冲用户IO等待导致CPU的等待成本,CPU和内存的交互是最频繁的,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状
基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。
Java虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致!
Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数)。
对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会涉及Java 栈、Java 堆、方法区这三个最重要内存区。
了解了内存模型,再来分析下垃圾回收机制。
垃圾回收机制
垃圾回收可分为两种方法:
1.引用计数法
每个对象创建时都会分配一个引用计数器,初始化赋值后该对象实例的引用计数为 1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。当对象被引用的个数为零时,意味着没有地方再使用这个对象,可以被回收掉了。但是这个方法存在严重的问题,就是无法检测“循环引用”:
public class MyObject {
public Object mObject = null;
public static void testGC(){
MyObject objA = new MyObject();
MyObject objB = new MyObject();
// 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0
objB.mObject = objA;
objA.mObject = objB;
objA = null;
objB = null;
System.gc();
}
}
由于这个严重问题,目前已经不再采用这种方式。
2.可达性分析法
可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。如何判断对象是否可达呢?采用了根搜索算法,从一个叫做GC ROOT节点开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
哪些节点可以被标记为GC ROOT节点呢,主要有以下四种:
1.JAVA栈(帧栈中的本地变量表)中引用的对象。
2.方法区中静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中引用的对象(Native对象)
知道哪些节点可达、哪些节点是无用节点后,就需要真正的完成垃圾回收,垃圾回收算法有以下几种:
3.垃圾回收算法
假设当前内存映射如下,黑色的表示可回收对象,灰色表示存活对象,绿色表示未使用空间:
1.标记—清除算法
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段,首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,但是垃圾回收后会有很多内存碎片。
2.标记—整理算法
标记—整理算法标记方式和标记—清除算法是一样的,但对标记出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。这样可以避免内存碎片,但是需要创建多个句柄。
3.标记-复制算法
标记-复制算法将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。这种做法不容易产生碎片,简单粗暴,但是,它意味着你在一段时间内只能使用一部分的内存,超过这部分内存的话就意味着堆内存里频繁的复制清空,性能将会降低。
Java堆内存垃圾回收
JAVA垃圾回收主要是针对堆内存的回收,JAVA堆内存区域基于Generation算法(Generational Collector)划分为新生代、年老代和持久代:
新生代
新生代又被进一步划分为Eden和Survivor区,Survivor区又进一步划分为S0和S1区。
几乎所有新生成的对象首先都是放在年轻代的,当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC),GC的同时会把E区的存活的对象复制到S0区,同时清空E区;如此往复,如果S0区也存放满了时,则将E区和S0区存活对象复制到S1区,然后清空Eden和S0区,此时S0区是空的,然后将S0区和S1区交换,即保持S1区为空, 如此往复。当S1区不足以存放 E区和S0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
老年代
当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15,就会移动到老年代中。老代中存放的都是一些生命周期较长的对象,通常来说老年代的内存空间也要比新生代的大得多(通常是2:1),一般来说,大对象会被直接分配到老年代。
持久代
用于存放静态文件(class类、方法)和常量等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
常见可回收对象
总结一下平常遇到的比较常见的将对象判定为可回收对象的情况:
1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象
2)局部引用所指向的对象
3)只有弱引用与其关联的对象
网友评论