Java虚拟机区域
运行的时候讲内存分为多个不同的数据区域
程序计数器
比较小的内存空间,可以看成当前程序所执行的字节码的行号指示器。
虚拟机栈
描述了Java方法执行的内存模型,每个方法执行的时候会创建栈帧,存储局部变量表、操作数、动态连接、返回地址等。调用方法的过程是一个进栈和出栈的过程。
异常说明:线程请求栈深度大于虚拟机所允许的情况下,抛出StackOverflowErro异常;当栈可以动态扩展的时候,也可能抛出OutOfMemoryError
本地方法栈
为虚拟机使用到的Native方法提供服务
堆
虚拟机所管理的内存中最大的一块。被所有的线程所共享,虚拟机启动的时候创建。用于存放对象的实例。也是垃圾收集器的主要工作区域。堆中是分代的:新生代,老年代。
主流虚拟机都可以动态扩展空间,可以通过(-Xmx -Xms)参数控制。
方法区
各个线程共享的内存区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
运行时常量池是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。
注意:运行期也可以将新的常量放入常量池中
虚拟机中的对象
对象的创建
当遇到一个new指令的时候,虚拟机是如何工作的那
- 常量池中检查是否存在此类的符号引用,并检查这个类是否被加载、解析、初始化过
- 如果没有:进行类的加载过程
- 类加载检查完成之后,虚拟机将为新对象分配内存。
说明:内存分配存在指针碰撞和空闲列表两种方法。虚拟机采用CAS和失败重试保证内存分配操作的原子性。还可以使用本地线程缓冲的方法(-XX:+/-UseTLAB) - 分配好的对象内存空间初始化为0
- 对对象进行必要设置,设置在对象头
- 执行初始化函数,完成对象初始化操作
对象在内存中的布局
内存中的对象有三个区域:对象头;实例数据;对齐填充
- 对象头:包括对象自身的运行时数据,类型指针(指向它的类元数据)
- 实例数据:代码中定义的各种类型的字段内容
- 对齐填充:并非必须,占位。整个对象必须满足8字节的整数倍
对象的访问定位
程序会通过栈上的引用访问堆中的对象,如何通过引用去定位对象规范没有具体定义。
- 句柄:在堆中分配句柄池,reference中存储的是句柄的地址,句柄中包含具体类的信息
- 直接地址访问:reference中存储的是直接的对象地址
虚拟机的溢出
堆溢出
不断创建对象,并且保证对象不被回收,产生堆溢出。通过VM参数限制堆不能自动扩展(Xms与Xmx相同)
import java.util.ArrayList;
import java.util.List;
/**
* 产生堆内存不足异常
* VM Args : -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*
* @author linxm
*
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
for(;;){
list.add(new OOMObject());
}
}
}
栈溢出
/**
* 产生栈溢出
* VM Args:-Xss128k
*
* @author linxm
*
*/
public class JavaVMStackOF {
private static int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
JavaVMStackOF oom = new JavaVMStackOF();
try{
oom.stackLeak();
}catch(Throwable e){
System.out.println("堆栈深度:" + stackLength);
throw e;
}
}
}
方法区溢出
通过Spring动态产生大量的类
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
/**
* Java方法区产生溢出
* VM Args: -XX:PermSize=2M -XX:MaxPermSize=2M
*
* @author linxm
*
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while(true){
Enhancer enchancer = new Enhancer();
enchancer.setSuperclass(OOMObject.class);
enchancer.setUseCache(false);
enchancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2,
MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
}
}
class OOMObject{
}
}
本机直接溢出
直接内存可以通过 -XX: MaxDirectMemorySize指定,如不指定则默认与堆最大值(Xmx)大小相同。在使用NIO的时候,有可能会有这种溢出
import java.lang.reflect.Field;
/**
* 直接内存溢出
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*
* @author linxm
*
*/
public class DirectMemoryOOM {
private static final int _1Mb = 1024 * 1024;
@SuppressWarnings("restriction")
public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
Field unsafeField = sun.misc.Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe)unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1Mb);
}
}
}
垃圾回收
需要思考3件事情
- 那些内存需要回收
- 什么时候回收
- 怎么回收
虚拟机中程序计数器,本地方法区,虚拟机栈随着线程而消亡;栈中的栈帧随着方法调入和调出而产生和消亡。
垃圾回收主要考虑的是堆和方法区
什么需要回收
堆中的对象实例是主要回收的内容,需要判断是否不再被使用。主流的虚拟机都是通过可达性算法来实现。
通过GC Root对象为起点,从这些节点开始搜索,走过的路径就是引用链,如果一个对象没有任何引用链可以连接到GC Root则判断为需要回收
GC Root包括如下几种:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
以上是判断什么需要回收的条件,所有的描述都是围绕着“引用”来讨论的。
关于引用又有4中强度的区分:
- 强引用:只要存在就不会被回收
Object.obj = new Object();
- 软引用:还有用但并非必须的对象,系统会在要发生内存溢出之前对这些对象进行二次回收。SoftReference来实现
- 弱引用:有用但并非必须,对象只能生存到下一次垃圾回收之前。WeakReference来实现
- 虚引用:最弱的一种引用关系,无法通过虚引用获取对象实例。只是在回收的时候收到一个系统通知。PhantomReference来实现
import java.lang.ref.SoftReference;
/**
* 演示软引用的使用方法
*
* @author Administrator
*
*/
public class fReferenceType {
public static void main(String[] args) {
// 这是一个强引用
Object o = new Object();
System.out.println(o.hashCode());
// o会在适当时机被回收
o = null;
Object objRef = new Object();
System.out.println(objRef.hashCode());
// 软引用
SoftReference<Object> aSoftRef = new SoftReference<Object>(objRef);
// SoftReference依然保持有objRef的引用,不会马上回收,但是在OutOfMemoryError之前回收对象
objRef = null;
// 回收前可以依然获取对象
objRef = aSoftRef.get();
System.out.println(objRef.hashCode());
}
}
什么时候回收
对象不可达表示可以回收,但是不是马上就回收,只是“死缓”而已。真正到达死亡的过程,需要两次标注:
发现对象不可达,进行一次标注并且进行一次筛选:看对象是否覆盖了finalize(),如果覆盖了并且虚拟机没有执行过finalize方法,则放入F-Queue队列中,表示等待销毁。finalize方法中可以拯救对象,只要这个时候与GC Root产生引用链,依然可以摆脱死亡。
/**
* 拯救对象的方法
* 1. 对象在GC时候自救
* 2. 对象自救只有一次机会,因为一个对象的finalize()方法只执行一次
*
* @author linxm
*
*/
public class FinalizeEspaceGC {
public static FinalizeEspaceGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("我还活着!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize方法被执行了!");
SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEspaceGC();
SAVE_HOOK = null;
// 第一次拯救自己
System.gc();
// 因为finalize方法优先级比较低,暂停一会
Thread.sleep(1000);
if(SAVE_HOOK == null){
System.out.println("拯救自己失败");
}else{
SAVE_HOOK.isAlive();
}
SAVE_HOOK = null;
// 第二次拯救自己
System.gc();
// 因为finalize方法优先级比较低,暂停一会
Thread.sleep(1000);
if(SAVE_HOOK == null){
System.out.println("拯救自己失败");
}else{
SAVE_HOOK.isAlive();
}
}
}
任何对象的finalize系统只会执行一次!
回收方法区
方法区也就是永久代也有可能进行垃圾回收。主要是回收废弃的常量和无用的类,但是效率很差。
- 判断是否为废弃的常量比较简单,只要没有常量的引用即可
- 判断是否为无用的类比较麻烦,至少需要满足下面的条件
- 类的所有实例已被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法通过反射构造此类
是否进行类的回收通过参数可以设置
-Xnoclassc
在大量使用反射和自定义ClassLoader的时候,需要虚拟机具有自动装卸类的功能
怎么回收垃圾
下面看一下垃圾回收的算法。
- 标记-清除算法:效率不高,空间碎片
- 复制算法:使用一半的内存,交替使用。现代虚拟机不使用1:1的内存方式。使用一块大的Eden空间和两个小的Survivor空间。HotSpot默认的比例是8:1,当Survivor内存不够的时候,就会使用老年代进行分配担保。
- 标记-整理算法:一般用于老年代
- 分代收集算法:java的堆分为新生代和老年代,采用不同的算法。新生代使用复制算法;老年代使用标记清除或者标记整理。
算法实现
枚举GC Root的算法,如何尽可能不造成卡顿是一个难题。
我们使用OopMap的结构,在虚拟机中存储引用关系,以供GC Root算法进行查看。不可能对每一个指令都生成OopMap(消耗太多的资源),这就有了安全点的概念,在安全点上生成OopMap,安全点选取的频率是一个对性能影响比较大的因素。
程序运行到安全点停下来的方法:抢占式中断,主动式中断(设置中断标志,各个线程轮休这个中断标志)
在进行GC的时候
- JVM对所有线程发起中断请求
- 如果程序的中断点不在安全点上,则运行到安全点
- 对于一些处于Sleep或者Blocked状态的线程,无法接收到JVM的中断请求,这时候需要使用安全区域(在一段代码中,引用关系不会发生变化)的概念来解决
垃圾回收器
有些垃圾回收器可以搭配使用,连线的表示可以配合使用
重要的概念:
- Minor GC
又称新生代GC,指发生在新生代的垃圾收集动作;
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
触发条件:当Eden区满时,触发Minor GC。 - Full GC
又称Major GC或老年代GC,指发生在老年代的GC;
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上;
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
垃圾回收器的具体说明
- Serial收集器:
单线程,会暂停所有的工作进行垃圾回收,默认用于Java client中。设置参数:-XX:+UseSerialGC - ParNew:
Serial的多线程版本。设置参数
-XX:+UseConcMarkSweepGC,指定使用CMS后,会默认使用ParNew作为新生代收集器
-XX:+UseParNewGC,强制指定使用ParNew
-XX:ParallelGCThreads,指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同 - Parallel Scavenge:
新生代的收集器,使用复制算法,并行的多线程收集器。主要的特点是控制吞吐量。
-XX:MaxGCPauseMillis,用于设置回收器的停顿时间;
-XX:GCTimeRatio,用于设置吞吐量的大小;
-XX:+UseAdaptiveSizePolicy,打开这个参数则系统会自动设置很多参数,根据GC运行的情况进行调节(自省是这个收集器的主要特点) - Serial Old:
Serial收集器的老年代版本,可以作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用 - Parallel Old:
Parallel Scavenge收集器的老年代版本 - CMS
是一种以最小停顿为目标的设计方案,是基于标记-清除的算法
-XX:+UseConcMarkSweepGC,指定使用CMS收集器
-XX:+UseCMSCompactAtFullCollection,打开这个参数则在需要Full GC的时候使用内存碎片的合并整理过程
-XX:+CMSFullGCsBeforeCompaction,设置执行多少次不压缩的Full GC后,来一次压缩整理,为减少合并整理过程的停顿时间,默认值为0 - G1
面向服务端应用的收集器,并行与并发;分代收集;空间整合;可预期的停顿
-XX:+UseG1GC,指定使用G1收集器;
-XX:InitiatingHeapOccupancyPercent,当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
-XX:MaxGCPauseMillis,为G1设置暂停时间目标,默认值为200毫秒;
-XX:G1HeapRegionSize,设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;