1. 运行时数据区域
程序计数器(Program Counter Register)
这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址,对于JVM中的程序计数器可以看做是当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器(这很好理解,每个线程都有在执行任务,如果线程切换后要能保证能恢复到正确的位置),重要的一点——程序计数器,这是JVM规范中唯一一个没有规定会导致OutOfMemory(内存溢出,OOM)的区域。
虚拟机栈(Java Virtual Machine Stacks)
这块内存区域就是我们常常说的“栈”,我们所熟知的是它用于存放变量,也就是说例如:
int i= 0; |
虚拟机栈内存就会用4个字节来存储i变量。对于变量的内存空间是一开始就能确定的(对于引用型变量,它当然存储的就是一个地址引用,其大小也是固定的),所以这块内存区域在编译器就能够确定下来,这块区域可能会抛出StackOverflowError或者OOM错误。设置JVM参数”-Xss228k”(栈大小为228k)。
对于单线程情况下,无论如何抛出的都是StackOverflowError。如果要抛出OOM异常,导致的原因是不断地在创建线程,直到将内存消耗殆尽。
JVM的内存由堆内存 + 方法区内存 + 剩余内存,也就是剩余内存=操作系统分配给JVM的内存 - 堆内存 - 方法区内存。-Xss设置的是每个线程的栈容量,也就是说可以创建的线程数量 = 剩余内存 / 栈内存。此时如果栈内存越大,可以创建的线程数量就少,就容易出现OOM;如果栈内存越小,可以创建的线程数量就多,就不容易出现OOM。
要避免这种情况最好就是减少堆内存+方法区内存,或者适当减少栈内存。对于栈内存的配置,一般采用默认值1M,或者采用64位操作系统以及64位的JVM。
本地方法栈(Native Method Stack)
本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。
Java堆(Java Heap)
对于堆,Java程序员都知道对象实例以及数组内存都要在堆上分配。堆不再被线程所独有而是共享的一块区域,它的确是用来存放对象实例,也是垃圾回收GC的主要区域。实际上它还能细分为:新生代(Young Generation)、老年代(Old Generation)。对于新生代又分为Eden空间、From Survivor空间、To Survivor空间。至于为什么这么分,这涉及JVM的垃圾回收机制,在这里不做叙述。堆同样会抛出OOM异常,设置JVM参数” -Xms20M -Xmx20M“(前者表示初始堆大小20M,后者表示最大堆大小20M)可以设置堆内存的大小。
方法区(Method Area)
对于JVM的方法区,可能听得最多的是另外一个说法——永久代(Permanent Generation),呼应堆的新生代和老年代。方法区和堆的划分是JVM规范的定义,而不同虚拟机有不同实现,对于Hotspot虚拟机来说,将方法区纳入GC管理范围,这样就不必单独管理方法区的内存,所以就有了”永久代“这么一说。方法区和操作系统进程的正文段(Text Segment)的作用非常类似,它存储的是已被虚拟机加载的类信息、常量(从JDK7开始已经移至堆内存中)、静态变量等数据。设置JVM参数为”-XX:MaxPermSize=20M”(方法区最大内存为20M)。
字符串常量池在JDK6的时候还是存放在方法区(永久代)所以它会抛出OutOfMemoryError:Permanent Space;而JDK7后则将字符串常量池移到了Java堆中,上面的代码不会抛出OOM,若将堆内存改为20M则会抛出OutOfMemoryError:Java heap space;至于JDK8则是纯粹取消了方法区这个概念,取而代之的是”元空间(Metaspace)“,所以在JDK8中虚拟机参数”-XX:MaxPermSize”也就没有了任何意义,取代它的是”-XX:MetaspaceSize“和”-XX:MaxMetaspaceSize”等。
直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
2. 垃圾收集
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
判断一个对象是否存活
1. 引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数不为 0 的对象仍然存活。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
2. 可达性分析算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
3. 引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 具有四种强度不同的引用类型。
(一)强引用
被强引用关联的对象不会被垃圾收集器回收。
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object(); |
(二)软引用
被软引用关联的对象,只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj = null; // 使对象只被软引用关联 |
(三)弱引用
被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集。
使用 WeakReference 类来实现弱引用。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; |
WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> |
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。Concu