JAVA通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,每个线程又单独地有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理。
一、JVM逻辑内存模型的构成
1.1 程序计数器 Program Counter Register
当前线程所执行的字节码指令的地址。字节码解释器工作时,依赖于改变计数器的值来读取下一条需要执行的字节码指令。
如果线程正在执行的是一个JAVA方法,计数器的值就是正在执行的字节码指令的地址,如果是NATIVE方法,这个计数器的值为空Undefined。
为了线程切换后能恢复到正确的执行位置 ,每条线程有一个独立的程序计数器,各计数器互不影响。——线程私有的内存。
唯一 一个在JVM规范中没有规定任何OUTOFMEMORYERROR情况的内存区域。
1.2 JVM栈 Java Virtual Machine Stacks
线程私有,与线程的生命周期相同。
JVM栈是JAVA方法执行的内存模型:每个方法被执行时,会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直到执行完成的过程,就对应着一个栈帧在JVM栈中从入栈到出栈的过程。
【局部变量表】存放编译期可知的基本数据类型、对象引用,其中64位长度的long和double类型的数据会占用2个局部变量空间slot,其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,该方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变变量表的大小 。
JVM栈规定了两种异常情况:1. 如果线程请求的栈深度大于JVM允许的深度,抛StackOverflowError异常;2. 如果JVM栈可以动态扩展,当扩展时无法申请到足够的内存时,抛OutOfMemoryError异常。
1.3 本地方法栈 Native Method Stacks
与jvm栈的区别是,本地方法栈为JVM使用的Native方法服务。jvm规范对本地方法栈没有强制规定,各虚拟机可自由实现,有些虚拟机直接把本地方法栈和JVM栈合二为一,如Sun HotSpot虚拟机。
同样抛出两种异常:StackOverflowError,OutOfMemoryError。
1.4 Java堆 Java Heap
堆是JVM管理的内存中最大的一块。被所有线程共享,在JVM启动时创建。唯一目的是存放对象实例。绝大多数对象实例在这里分配内存。
Java堆是垃圾回收器管理的主要区域,因此,多数时候也被称为GC堆。
现在的回收器基本采用分代回收的算法,所以java堆进一步细分为:新生代和老年代。
根据JVM规范,Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可,就像磁盘一样。当前主流虚拟机的实现都是可扩展的(通过 -Xmx和-Xms控制)。
如果堆中没有可用内存,也无法扩展时,抛OutOfMemoryError异常。
1.4 方法区 Method Area
各线程共享。用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
jvm规范对这个区域的限制非常宽松,除了和堆一样,不需要连续的内存、可以选择固定大小或可扩展外,还可以选择不实现垃圾回收。
一般垃圾回收行为在这个区域较少出现,回收目标主要是针对常量池的回收和对类型的卸载,条件相当苛刻。但回收仍是必要的,否则可能导致内存泄漏。
当方法区无法满足内存分配需求时,抛OutOfMemoryError异常。
1.5 运行时常量池 Runtime Constant Pool
是方法区的一部分。
Class文件中,除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池Contant Pool Table,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池具备动态性,java语言并不要求常量一定只能在编译期产生,运行期间也可能将新的常量放入池中。
运行时常量池是方法区的一部分,同样受到方法区内存的限制。也会抛OutOfMemoryError异常。
1.6 直接内存 Direct Memory
不是jvm运行时数据区的一部分,也不是jvm规范中定义的内存区域。但也可能导致OutOfMemoryError异常的出现。
JDK1.4新增了nio,引入了一种基于通道Channel与缓冲区Buffer的IO方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆里的directByteBuffer对象作为这块内存的引用来操作。这样可以避免在java堆和native堆中来回复制数据,在一些场景中显著提高性能。
直接内存的分配不受jvm的限制,但受本机总内存的大小及处理器寻址空间的限制。OutOfMemoryError异常。
二、对象访问
Object obj = new Object();
1)Object obj, 反映到java栈的本地变量表中,作为一个引用类型数据出现。
2)new Object(),反映到java堆中,形成一块存储了object类型的所有实例数据值的结构化内存。根据具体类型及jvm实现的对象内存布局的不同,这块内存的长度是不固定的。java堆中还必须包含能查找到此对象类型数据的地址信息。类型数据是指对象类型、父类、实现的接口、方法等信息,这些类型数据是存储在方法区中的。即:类型数据在方法区中,对象在堆中。
3)根据引用定位对象。不同虚拟机实现的对象访问方式不同,主流的访问方式有:使用句柄、直接指针。
【句柄访问方式】Java堆中划分出一块内存作为句柄池。引用中存储的是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息。
【直接指针方式】引用中直接存储对象地址。这样,堆对象的布局中就必须考虑存储类型数据的具体地址。
【对比】句柄访问的最大好处是引用中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,引用本身不用修改。而直接指针的最大好处是访问速度快,节省了一次指针定位的时间开销。对于SunHotSpot而言,使用直接指针方式。
三、垃圾回收
3.1 确定垃圾对象
【引用计数法】——python
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1,当引用失效时,计数器的值就减1,任何时刻计数器为0的对象就是不可能在被使用了。
实现简单,效率较高,但无法解决循环引用的问题。(可能存在两个对象互相引用对方,但没有第三者指向它们中的任意一个,最终它们的引用计数都非0,永远不会回收它们)
【可达性分析法】——java
以”GC Roots”对象为起点进行搜索。从这些节点开始向下搜索,搜索所有走过的路径为引用链,当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的。
不可达对象不一定就会成为可回收对象。——两次标记
可作为GC Roots的对象包括:1)JVM栈(本地变量表)中引用的对象;2)方法区中类静态属性引用的对象;3)方法区中常量引用的对象;4)本地方法栈中JNI引用的对象。
【引用类型】
强引用:普遍存在的,譬如Object obj = new Object();,只要强引用还在,就永远不回收对象。
软引用:一些有用但并非必需的对象。在系统将要发生内存异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果第二次回收还是没有足够的内存,才会抛出内存异常。
弱引用:非必需对象。当垃圾回收器工作时,无论当前内存释放是否足够,都会回收掉只被弱引用关联的对象。
虚引用:即幽灵引用或者幻影引用。虚引用完全不影响对象的生存时间,也无法通过虚引用来取得一个对象实例 。对一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
四、垃圾回收算法
4.1 标记-清除算法 Mark-Sweep
参考资料
http://blog.csdn.net/jiangwei0910410003/article/details/40709457
参考资料