Java 内存区域
我们通常说的Java内存主要指的在Java程序运行的过程中Java虚拟机把它所管理内存区域划分的不同的数据区域。这些不同的区域保存着不同的数据类型,有着不同的创建和销毁时间。Java的内存区域可以分为两大部分:方法区(Method Area)和堆(Heap) 各个线程公用的内存区域,俗称堆内存(不连续的内存空间);虚拟机栈(VM Stack) 、本地方法栈(Native Method stack) 、程序计数器(Program Counter Register)各个线程私有的,生命周期与线程保持一致,俗称栈内存,先进后出(连续的内存空间)。
程序计数器 :程序计数器是当前线程所执行的字节码的行好的指示器,是一块较小的内存区域。Java虚拟机的多线程是通过线程的轮流切换来实现的,在任何一个时刻,处理器的一个内核只能执行一个线程的指令,程序计数器保证了当前线程再次被执行时能执行到正确的位置,因此每个线程都会有一个独立的程序计数器,由于占用内存较小,此区域不会发生内存溢出。
-
虚拟机栈:描述的是Java内存区域执行的内存模型:每个方法在执行过程中都会创建一个栈帧,用来保存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从开始执行到执行结束都对应着一个栈帧在虚拟机中从入栈到出栈的过程。
- 通常所说的“栈”就是指的是虚拟机栈中的局部变量表部分。该部分保存了各种基本数据类型,对象的引用(reference),returnAddress类型(指向了一条字节码指令的地址)。
本地方法栈:类似于虚拟机栈,为的是虚拟机执行Native方法服务。
堆:在虚拟机启动时创建,是所有线程共享的一块内存区域。存放了所有的new出来的对象的实例和数组,对象的reference则在虚拟机栈上。这一块也是垃圾回收主要回收的区域,通常容易发生内存泄漏(分配出去的内存不能被回收,失去了对这块内存的控制。eg:保存了reference,但是一直没有使用)和内存溢出(程序需要的内存超出了虚拟机所分配的内存)。
方法区:与Java堆一样,是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
内存分配例子分析
public void test(){
Person person = new Person();
}
这段代码涉及Java堆、虚拟机栈、方法区三个地区的内存分配。new Person()会在Java堆上开辟一块内存,创建了Person这个类的实例;person这个引用则会在虚拟机栈的本地变量表中,并指向Java堆上我们刚创建的对象;同时方法区中还保存了能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等)。
Java 对象的创建
Java是一门面向对象的语言,在虚拟机运行时时刻都有对象被创建出来。在new 创建对象时,虚拟机首先会检查对象对应的类是否被加载了出来,如果没有会先执行类加载过程。类加载完成后,对象所需的内存大小已经确定了下来,为对象分配内存有两种方式:
- 指针碰撞:Java堆内存是规整的,在已用内存和未用内存中间有个指针,每次新分配一块内存,指针会偏移一段,保持在已用和未用内存之间。
- 空闲列表:Java堆内存不是规整的,已用和未用内存相互交错,虚拟机在分配内存时必须维护了一个列表,上面记录了已用和未用的内存区域,每次分配内存时都从列表中找到一块足够大的内存来分配内存。
在对象内存分配结束后,虚拟机会为对象分配到的内存空间都初始化为0,所以不进行赋值就能访问到对象的属性字段的零值。
Java 对象的定位
访问对象需要通过栈上的reference类型引用堆内存上的对象,引用方式分为两种:
- 使用句柄:Java堆上划分出一块句柄内存,reference指向句柄,而句柄指向对象和方法区上的类型数据。
- 直接引用:reference直接指向对象。
Java 内存分配和垃圾回收
Java内存结构中,虚拟机栈,程序计数器,本地方法栈这三个区域随线程而生灭,栈中的栈帧随着方法的进入和退出而进行出入栈操作,每一个栈帧需要的内存在类结构确定下来的时候就已经确定了,这几个区域不需要考虑到垃圾回收的问题。而Java堆和方法区只有在运行时才可以确定内存的大小,这部分内存的分配和回收都是动态的,垃圾回收机制处理的主要是这一部分内存。
哪些对象会被回收
判断对象能否被回收主要有两种方式:
- 引用计数器法:给对象添加一个引用计数器,当被引用一次后计数器就加1,在进行垃圾回收的时候,如果一个对象的引用计数器为0,则进行回收,引用计数器存在一个严重的问题是,当两个对象相互引用,但是这两个对象都已经不可用时依然不能被回收,如下:
class TestGC1{
public Object instance= null;
}
class TestGC2{
public Object instance= null;
}
public testGC(){
TestGC1 tsGC1 = new TestGC1();
TestGC2 tsGC2 = new TestGC2();
tsGC1.instance = tsGC2;
tsGC2.instance = tsGC1;
tsGC1= null;
tsGC2= null;
}
如方法testGC()中,tsGC1和tsGC2的instance相互引用着,
但tsGC1和tsGC2已经不能再被访问,如果采用引用计数器法,这两个无用的对象是不可能被回收的。
- 可达性分析算法:从一系列可以作为 GC Roots 的对象开始向下搜索,搜索走过的路径称为引用链,当GC回收时,一个对象没有通过引用链与任何GC Roots对象连接,则这个对象就可以被回收了。可作为GC Roots对象的有以下几种:
- 虚拟机栈的本地变量表中引用的对象。
- 方法区中静态属性和常量引用的对象。
-
本地Native方法引用的对象。
如图所示,如果按照可达性分析算法,在tsGC1和tsGC1没有为null时,对象TestGC1和TestGC2都可以作为GC Root而不被回收的,当tsGC1和tsGC1都为null时,由于已经没有局部变量表中的对象对对象TestGC1和TestGC2进行引用,所以此时对象TestGC1和TestGC2都不为GC Root,此时虽然两个对象的Instance相互引用,但是没有与任何GC Roots相连接,是可以直接被垃圾回收进行回收的。
Referencce引用
在JDK1.2之前,指的如果reference类型中存储的数值代表的是另一块内存地址,则称这块内存地址为引用,在JDK1.2之后增加了三种不同的引用类型,现在共四种:
- 强引用:通过new 关键字创建的引用,只要引用存在,一般不会被回收。
- 软引用:在第一次GC发生时,进行标记,如果第二次GC时,如果内存仍然不足,就会进行回收。
- 弱引用:在第一次GC发生时,进行标记,第二次GC时,无论内存是否充足,都会进行回收。
- 虚引用:随时可能被回收,作用只是在被回收时通知系统。
内存分配与回收策略
内存的分配:将Java堆分为新生代和老年代,新生代又分为Eden区和两个Survivor区,大小 比例为8:1:1,每次小对象直接分配在新生代的Eden和其中一个Survivor区,比较大的对象直接进入老年代。
内存的回收:在新生代进行的Minor GC,回收时将还在存活的对象复制到Surivior区域,Eden区全部回收完成后将对象复制回来(增加了回收后新对象分配内存时的效率,因为堆上内存的不是连续的);老年代的回收:对无用的对象标记后统一进行回收。
什么对象会被回收
当一个对象不再和任何GC Roots联系时,那么将会被第一次标记并进行筛选,当这个对象没有重写finalize()方法或者finalize()方法已经执行过了,这时候就会被直接标记,如果这个对象的finalize()方法没有执行,那么这个对象将会被放置在一个F-Queue队列中,并在稍后触发这个方法进行执行,如果这个对象在finalize()方法中重新拯救了自己(例如被某个关联上),那么在第二次进行标记的时候,该对象会被移除回收队列,如果没有,就会被回收。