JVM

1、JVM 的内存模型以及分区情况和作用

如下图所示:

JVM 内存结构

黄色部分为线程共有,蓝色部分为线程私有。

  • 方法区:用于存储虚拟机加载的类信息,常量,静态变量等数据。
  • :存放对象实例,所有的对象和数组都要在堆上分配。是 JVM 所管理的内存中最大的一块区域。
  • :Java 方法执行的内存模型:存储局部变量表,操作数栈,动态链接,方法出口等信息。生命周期与线程相同。
  • 本地方法栈:作用与虚拟机栈类似,不同点本地方法栈为 native 方法执行服务,虚拟机栈为虚拟机执行的 Java 方法服务。
  • 程序计数器:当前线程所执行的行号指示器。是 JVM 内存区域最小的一块区域。执行字节码工作时就是利用程序计数器来选取下一条需要执行的字节码指令。

2、Java 内存分配

  • 寄存器:无法控制。
  • 静态域:static 定义的静态成员。
  • 常量池:编译时被确定并保存在 .class 文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法的名称和描述符)。
  • 非 RAM 存储:硬盘等永久存储空间。
  • 堆内存:new 创建的对象和数组,由 Java 虚拟机自动垃圾回收器管理,存取速度慢。
  • 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性。

3、Java 堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?

JVM 的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。

堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。


4、Java 中会存在内存泄漏?简述一下

所谓内存泄漏就是指一个不再被程序使用的对象或变量一直被占据在内存中。

Java 中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉。由于 Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么 GC 也是可以回收它们的。例如下面的代码可以看到这种情况的内存回收:

import java.io.IOException;

public class JVMTest {

	public static void main(String[] args) throws IOException {
		try {
			gcTest();
		} catch (IOException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		System.out.println("has exited gcTest!");
		System.in.read();
		System.in.read();
		System.out.println("out begin gc!");
		
		for (int i = 0; i < 100; i++) {
			System.gc();
			System.in.read();
			System.in.read();
		}
	}
	
	private static void gcTest() throws IOException {
		System.in.read();
		System.in.read();
		Person p1= new Person();
		System.in.read();
		System.in.read();
		Person p2= new Person();
		p1.setMate(p2);
		p2.setMate(p1);
		System.out.println("before exit gctest!");
		System.in.read();
		System.in.read();
		System.gc();
		System.out.println("exit gctest!");
	}
	
	private static class Person {
		byte[] data= new byte[20000000];
		Person mate= null;
		public void setMate(Person other) {
			mate= other;
		}
	}
}

Java 中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是 Java 中可能出现内存泄漏的情况。例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局 map 对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。

检查 Java 中的内存泄漏,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄漏。

如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

下面代码的主要特点是清空堆栈中的某个元素,并不是彻底把它从数组中拿掉,而是把存储的总数减少。

import java.util.EmptyStackException;

public class Stack {

	private Object[] elements= new Object[10];
	private int size= 0;
	public void push(Object e) {
		ensureCapacity();
		elements[size++]= e;
	}
	
	public Object pop() {
		if(size== 0) throw new EmptyStackException();
		return elements[--size];
	}
	
	private void ensureCapacity() {
		if(elements.length== size) {
			Object[] oldElements= elements;
			elements= new Object[2 * elements.length+ 1];
			System.arraycopy(oldElements, 0, elements, 0, size);
		}
	}
	
}

上面的原理应该很简单,假如堆栈加了 10 个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这个对象是无法回收的,这个才符合了内存泄漏的两个条件:无用,无法回收。但是就是存在这样的东西也不一定会导致什么样的后果,如果这个堆栈用的比较少,也就浪费了几个 k 内存而已,反正我们的内存都上 G 了,哪里会有什么影响,再说这个东西很快就会被回收的,有什么关系。下面看两个例子:

public class Bad {
    public static Stack stack= new Stack();
    static {
        stack.push(new Object());
        stack.pop();				// 这里有一个对象发生内存泄漏
        stack.push(new Object());	// 上面的对象可以被回收了,等于是自愈了
    }
}

因为是 static,就一直存在到程序退出,但是我们也可以看到它有自愈功能,就是说如果你的 Stack 最多有 100 个对象,那么最多也就只有 100 个对象无法被回收。其实这个应该很容易理解,Stack 内部持有 100 个引用,最坏的情况就是他们都是无用的,因为我们一旦放新的进去,以前的引用自然消失!

内存泄漏的另外一种情况:当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了。在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集