JVM内存区域划分->判断对象是否存活->垃圾回收算法(必考)->垃圾回收器(针对垃圾回收算法的实现)->JVM性能检测
单核CPU:每一个具体时刻只有一个线程抢占CPU资源。
局部变量表:存放基本数据类型(8种)、对象引用(占4个字节)
局部变量表所需的空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。
变量存放内存分析:
public class Test1{
public static void main(String[] args){
Test1 test1=new Test1();
int a=10;//10在常量池
}
}
public class Test1、public static void main(String[] args)-》类的信息, 放在方法区中
new Test1()-》堆中
test和a放在栈中
10->基本数据类型的值,放在运行时常量池。
一、OOM溢出和栈溢出
1.OOM溢出(OutOfMemory)
设置堆的最大值为20M:-Xmx20M
设置堆的最小值为20M:-Xms20M
如果最大值和最小值相等,说明不允许对堆进行扩容。
这个参数在Run-VM Options中设置。
OOM异常代码(内存泄漏):
import java.util.ArrayList;
import java.util.List;
class OOM{}
public class Test1{
public static void main(String[]args){
List<OOM> list=new ArrayList<>();
while(true){
list.add(new OOM());
}
}
}
OOM溢出分为两种:内存溢出(常见)和内存泄漏
内存溢出:内存中的对象确实还应该存活,但由于堆内存不够用产生的异常。
内存泄漏:无用对象无法被GC。
判别内存溢出和内存泄漏?
如果分配较多的内存可以解决,不再报错,则为内存溢出;否则仍然报错,为内存泄漏。显然,上面为内存泄漏。
2.栈溢出
设置参数:-Xss128K
import java.util.ArrayList;
import java.util.List;
/**
*OOM-Xss128K 栈溢出代码示例(单线程)
*/
public class Test1{
private int length=1;
public void stackTest(){
length++;
stackTest();
}
public static void main(String[]args){
Test1 test1=new Test1();
try{
test1.stackTest();
}catch(Throwable e){
e.printStackTrace();
System.out.println("栈的深度:"+test1.length);
}
}
}
结果:每次运行结果都可能不同。
如果默认不设置栈大小,大概是2万多。
多线程版本,可能会造成电脑死机、黑屏。
二、判断对象已死
1.引用计数法
算法思想:给每个对象附加一个引用计数器,每当有一个地方引用此对象,计数器+1;每当有一个引用失效时,计数器-1;在任意时刻,只要计数器值为0的对象就是不能再被使用的,即对象已死。
优点:引用计数法实现简单,判定效率也较高。Python使用引用计数法来管理内存。C++中的智能指针一部分也用到了。但是无法解决循环引用问题。(缺点)
JVM未采用此算法。
/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC(未置空,对象回收前)
*/
public class Test{
private Object instance;
private static int_1MB=1024*1024;
private byte[] bytes=new byte[2*_1MB];
public static void main(String[]args){
Test test1=new Test();
Test test2=new Test();
//循环引用
test1.instance=test2;
test2.instance=test1;
//test1=test2=null;
System.gc();
}
}
结果说明在垃圾回收前,有7433K;垃圾回收后,有4880K.
/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC(回收后)
*/
public class Test{
private Object instance;
private static int_1MB=1024*1024;
private byte[] bytes=new byte[2*_1MB];
public static void main(String[]args){
Test test1=new Test();
Test test2=new Test();
//循环引用
test1.instance=test2;
test2.instance=test1;
test1=test2=null;
System.gc();
}
}
结果:在垃圾回收前,有7433K;在垃圾回收后,剩余752K.说明Java虚拟机并没有采用引用计数法。
2.可达性分析算法
Java采用可达性分析算法来判断对象是否存活(C#、Lisp).
核心思想:
通过一系列“GC Roots”的对象作为起点,从这些节点开始向下搜索对象,搜索走过的路径,称为“引用链”,当一个对象到任意一个GC Roots对象没有任何的引用链相连时(从GC Roots到对象不可达),认为此对象已死。
在Java中能作为GC Roots的对象包含以下四种:
1)虚拟机栈中(局部变量表)引用的对象
2)类静态变量引用的对象
3)常量引用的对象
4)本地方法栈中引用的对象
在上面的例子中,由于test1和test2不是上面四种情况的一种,虽然它们互相指(引用),但是因为不是GC Roots对象,所以JVM认为它们已死。
3.引用的概念(*****)
JDK1.2之后,对于引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种强度依次递减。
3.1 强引用:指的是代码中普遍存在的,类似于Object obj=new Object()。
在JVM中,只要强引用还存在,垃圾回收器永远不会回收此对象实例。
3.2 软引用:软引用用来描述一些有用但不必须对象。对于仅被软引用指向的对象,在系统将要发生内存溢出前,会将所有软引用对象进行垃圾回收。若内存够用,这些对象仍然保留。在JDK1.2之后,提供了SoftReference来实现软引用。
3.3 弱引用:弱引用强度比软引用更差一点。仅被弱引用关联的对象最多只能生存到下一次GC之前。当垃圾回收器开始工作前,无论当前内存是否够用,都会回收掉仅被弱引用关联的对象。在JDK1.2之后,使用WeakReference来实现弱引用。
3.4 虚引用:也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是在这个对象被GC之前,收到一个系统通知。在JDK1.2之后,提供了PhantomReference来描述虚引用。
import java.lang.ref.SoftReference;
/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC
*软引用
*/
public class Test{
private Object instance;
private static int_1MB=1024*1024;
private byte[] bytes=new byte[2*_1MB];
public static void main(String[] args){
Test test=new Test();
SoftReference softReference=new SoftReference(test);//让软引用指向这块内存
test=null;
System.gc();
}
}
importjava.lang.ref.WeakReference;
/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC
*弱引用
*/
public class Test{
private Object instance;
private static int_1MB=1024*1024;
private byte[] bytes=new byte[2*_1MB];
public static void main(String[] args){
Test test=new Test();
WeakReference weakReference=new WeakReference(test);//让弱引用指向这块内存
test=null;
System.gc();
}
}
结果说明:垃圾回收后和软引用相比,剩余空间变小,说明被弱引用的对象已经被回收。
三、对象的自我拯救(finalize())
Object类的方法:
protected void finalize() throws Throwable{
}
}
在可达性分析算法中不可达的对象,也并非"非死不可",所有不可达的对象处于"缓刑"阶段。
要宣告一个对象的彻底死亡,需要经历两次标记 一次筛选过程:
1)若对象在进行可达性分析之后发现到GC Roots不可达,此对象会进行第一次标记并且进行一次筛选过程。筛选的条件是此对象是否有必要执行finalize()。当对象没有覆盖finalize()方法或finalize()方法已被JVM调用过,JVM会将此对象彻底宣判死亡。
2)筛选成功(对象覆写了finalize()并且未被执行过),会将此对象放入F-Queue,如果对象在finalize()中成功自救(此对象与任意一个GC Roots建立联系),则对象会在第二次标记时被移除回收集合,成功存活;若对象在finalize()中仍然与GC Roots不可达,宣告死亡。
/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC
*/
public class Test{
private staticTesttest;
public static void isAlive(){
if(test!=null){
System.out.println("I am alive");
}
}
@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("finalize method execute");
test=this;//成功自救
}
public static void main(String[] args){
test=new Test();
test=null;
System.gc();
try{
Thread.sleep(500);
}catch(InterruptedExceptione){
e.printStackTrace();
}
if(test!=null){
test.isAlive();
}else{
System.out.println("I am dead");
}
//=============================
test=null;
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
if(test!=null){
test.isAlive();
}else{
System.out.println("I am dead");
}
}
}
第二次对象死亡的原因:任何一个对象的finalize()只会被系统调用一次,因此第二次回收对象失败。finalize()不推荐使用,在JDK1.9之后,finalize()加上了@Deprecated。