一、JVM内存结构
java虚拟机在解析执行java程序的时候会把其管理的内存主要分成五块数据区域。
-
程序计数器
占用很少的内存空间,可以看做当前程序所执行的字节码的行号指示器,程序计数器通过改变计数的值来告知JVM选取下一条执行的指令,因为多线程中多个线程共享CPU时钟,为了不致使执行错乱,因此每个线程都有独立的程序计数器内存,该片内存为线程私有。
-
java虚拟机栈
俗称栈区,描述方法执行的内存模型,当一个方法被调用执行的时候,方法会被压入栈创建栈帧,用于存放变量、局部变量、操作数栈、动态链接、方法出口等信息,因为不同线程操作的方法不一样,即便是操作同一个方法执行的进度也不一样,因此不可能共享同一片栈内存,所以该片内存也是线程私有。
-
本地方法栈
本地方法栈和java虚拟机栈的作用类似,不过虚拟机栈服务于java方法,而本地虚拟机栈则服务于native方法。
-
堆区
主要用于存放对象实例(几乎所有的对象实例都会在堆区中分配内存),是虚拟机管理的内存中最大的一块,允许线程共享该片内存区域,也是java GC的主要内存区域。
-
方法区
用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码等数据,该片内存也允许线程共享。
二、JAVA GC机制
java垃圾回收的主要区域就是堆内存,样想探究java垃圾回收机制,首先要知道什么样的对象会被虚拟机回收内存,接着虚拟机什么时候开始垃圾回收。
- 什么样的对象会被回收
当一个对象初始化完成之后,栈内存中存放其引用类型,而堆内存中存放其实例数据,对应该类的类信息则被加载存放到方法区中(可以参看类加载器一文),初始化完成之后的实例对象会经历三种状态,分别是可达状态、可恢复状态和不可达状态,也只有当一个实例对象变成不可达状态之后,才会被java垃圾回收机制真正的回收内存。
1. 可达状态:实例对象存在直接引用,形如Object o = new Object();,说明该实例数据在堆区中占用的内存还在被引用、访问;
2. 可恢复状态:当实例对象失去引用的时候则进入可恢复状态,形如o = null;,此时java垃圾回收机制会标记该片内存为可回收,但在真正的执行内存回收操作之前会调用该对象的finalize方法(该方法定义在Object中),如果执行finalize方法使得对象恢复引用,则对象变回可达状态,否则对象进入不可达状态,等待垃圾回收;
3. 不可达状态:对象失去引用,并且垃圾回收调用该对象的finalize方法之后没有使该对象变回可达状态,则该对象进入不可达状态,此时对象永久性的失去引用,只能等待被java垃圾回收占用的内存。
- 什么时候回收
当java虚拟机认为内存紧张的时候,虚拟机才会进行垃圾回收,而虚拟机何时开始垃圾回收对程序员透明。java垃圾回收会自动执行,但是由虚拟机决定何时执行,因此对程序员而言何时进行垃圾回收则不可预知。
- 强制垃圾回收
当一个对象失去引用后,会进入可恢复状态,此时如果调用其finalize方法没有使其重回可达状态,则该对象会永久失去引用等待java GC回收内存,系统什么时候回收内存,对程序、程序员是透明的,由jvm来选择何时进行回收;但是程序中可以调用以下两种方式来进行强制内存回收,但是这种“强制”对jvm来说其实是一种“建议”,即便是写了强制回收的代码,jvm也不一定会立即回收内存资源。
1. System.gc();
2. Runtime.getRuntime().gc();
如以下代码示例,验证强制垃圾回收其实只是对虚拟机进行“建议”,而虚拟机什么时候开始真正回收内存无法预测。
package gc;
public class Man {
private static int count = 0;
private String name;
private int age;
private String gender;
public Man(String name, int age, String gender) {
// TODO Auto-generated constructor stub
this.name = name;
this.age = age;
this.gender = gender;
}
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
count++;
System.out.println("我被gc调用了"+count+"次!");
}
}
package gc;
public class Main {
public static void main(String[] args) throws Exception {
for(int i=0; i<10; i++){
new Man(i+"姓名", i, i+"性别");
/*
* 进行强制垃圾回收,以下语句等效于Runtime.getRuntime().gc();
*/
System.gc();
}
}
}
输出结果每一次都不一定相同,随机抽一次的输出结果如下:
/*
* 我被gc调用了1次!
* 我被gc调用了2次!
* 我被gc调用了3次!
* 我被gc调用了4次!
*/
结论:
程序
new
了
10
个无引用的匿名对象,每
new
一个无引用的对象都会“强制”系统回收内存,但是从程序多次执行的表现观察,调用
finalize
方法的次数基本上每次都不相同而且不一定等于
10
次或
0
次,所以得出结论:
java
强制回收不是真的强制,而是对
jvm
的一种“建议”,“建议”其现在进行
GC
操作,但是
jvm
什么时候真正开始回收内存,对程序、程序员而言是透明的,不可预测。
- finalize
在垃圾回收机制回收某个对象所占用的内存之前,通常会要求调用适当的方法来清理资源,如果没有明确的指定,则java提供了默认机制来清理该对象所占用的内存,这个机制就是finalize方法,默认的finalize方法在java.lang.Object中被定义,可以被任何类重写。当一个对象从可达状态变成可恢复状态的时候,垃圾回收机制就标记这片内存可回收,但在真正回收内存之前会调用该对象的finalize方法来决定是否收回内存,但是是否调用了finalize方法以及何时调用finalize方法对程序员来说是透明的,由虚拟机来决定,一般情况下,只有当虚拟机认为内存紧张需要释放更多的堆内存的时候,垃圾回收机制才会进行垃圾回收。
finalize:清理资源的默认机制,但该方法是否被调用以及何时被调用不可预测,由虚拟机决定。
1. 最好不要主动调用对象的finalize方法,应该交给垃圾回收机制来调用;
2. finalize方法不是一定会被执行的,并且当执行该方法出现异常,垃圾回收机制不会报告异常,虚拟机也不会感到异常,但finalize方法中异常后的代码不会被执行。
将以上程序Man类的finalize方法稍作修改,程序如下,你会发现抛出异常之后的程序不会被执行,但是系统不会收到任何的异常欣喜
<span style="font-family:Microsoft YaHei;"> @Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
/*
* 这里将抛出空指针异常
*/
String str = null;
System.out.println(str.length());
/*
* 异常之后,程序将不会继续执行,但是系统也不会收到异常信息
*/
count++;
System.out.println("我被gc调用了"+count+"次!");
}</span>
- java中的引用
为了更好的让java GC服务与我们,java语言提供了对对象的四种引用方式,更具引用的强弱顺序分别如下。
1. 强引用:对象有一个及其以上的引用,例如Object oo = new Object();
2. 软引用:java中通过SoftReference类来实现引用级别低于强引用,当系统内存足够时,起不会被系统回收;当系统内存不够使,其可能被回收;
3. 弱引用:通过WeakReference类实现,引用级别低于软引用,无论系统内存是否足够,系统进行垃圾回收的时候总是会回收其占用的内存;
4. 虚引用:通过PhantomReference类实现,引用级别最低,类似没有引用,主要用于跟踪对象被垃圾回收的状态;
需要重点关注的是,如果采用了除强引用之外的引用,那就一定要切断原对象的引用,否则引用不会达到预期的效果,以弱引用为例,引用的用法示例代码如下。
<span style="white-space:pre"> </span>public static void main(String[] args) {
/*
* 1. 建立引用对象str
* 2. 让str对象绑定弱引用
* 3. 切断str对象原来的强引用
* 效果:当系统进行垃圾回收的时候,无论内存是否足够,都会回收str所占用的内存
* 如果没有弱引用,则垃圾回收的时候不一定回收str所占用的资源
*/
String str = new String("弱引用示例");
WeakReference<String> wr = new WeakReference<String>(str);
str = null;
}
- 常用对象的判死算法
1. 引用计数法:通过引用计数器来判断对象是否被引用,例如有一个引用则引用计数器+1,一般情况下引用计数法还是非常靠谱的判死算法,但是也有一个致命的缺点——对象的相互引用会导致无法回收内存。
如以下代码所示,在A类中引用了B类对象,在B类中引用了A类对象,造成一个回环,导致引用计数法无法回收内存。
package demo;
public class A {
/*
* 在A类中引用了B类对象
*/
B b = new B();
}
package demo;
public class B {
/*
* 在B类中引用了A类对象
*/
A a = new A();
}
因此,当AB两类实例化之后,即便断开引用,但是AB两类中的成员变量ab也会在堆内存中互相引用,导致程序计数器结果不为0,从而无法回收垃圾,图示如下。
2. 可达性分析:类似树状结构,从“GC Root”节点开始通过引用链搜索,如果一个对象无路可达,则表示该对象处于可恢复状态,等待被GC回收内存资源。
- 垃圾收集算法
1. 标记-清除算法:先标记“不可达”状态的对象,然后统一回收内存资源;效率不高,同时会导致内存碎片化;
2. 复制算法:为了提高“标记-清楚”算法的效率,将可用内存划分成大小相等的两块,每次只是用其中的一块,当这一块内存用完了(包含存活对象、等待回收的对象和碎片占用的空间),则一次性拷贝到另一块,此时将会释放等待回收对象的内存空间,同时解决内存碎片问题,存活对象连续占用一片内存,再一次性清空原先的内存块;
3. 标记-整理算法:相比较“复制算法”,不会浪费50%的内存,算法不直接回收不可达对象占用的内存,而是让存活对象都向一端移动,然后清理释放边界的内存空间;
4. 分代收集算法:根据对象的存活周期在堆中将对象分为新生代和老年代,根据不同代特点不同选择不同的算法,新生代——绝大部分对象很快不可达需要回收内存,采用复制算法,每次只需要付出极少的复制代价;老年代——绝大部分对象长期保持可达状态,适合采用“标记-清理”或“标记-整理”算法。
三、补充
1. 随着操作系统的发展,32bit系统必然会逐渐被64bit系统取代。虽然java虚拟机在很早以前就已经推出了64bit的版本,但是直到目前,仍旧存在问题——java程序跑在64bit系统上系能消耗比32bit系统的多,多消耗20%左右内存,存在10%左右的性能差距;
2. 首先要注意,java GC只针对内存的回收,对类似数据库连接、io等物力资源的释放不会自动回收,均需要手动close掉;
3. 程序员无法精确的控制何时回收内存,只能给JVM“提供”建议或在代码上做一定的优化。
附注:
本文如有错漏之处,烦请不吝指正,谢谢!