上图是Java内存模型运行时区域图。
运行时数据区分:
1.程序计数器:
- 由于java是多线程的,为了能够线程上下切换后能够恢复到正确的折行位置,每条线程都要有一个独立的程序计数器,记录当前线程正在折行的方法的虚拟机指令,该区域没有OOM发生。
2.JAVA虚拟机栈(本地方法栈) - 线程私有的:
- 生命周期跟线程一样的,描述的是JAVA方法执行的内存模型,每个方法创建调用的时候会创建一个栈帧,由于存储局部变量表等信息。当进入一个方法,栈帧需要的内存是已经确定的。
会发生Stackoverflow和oom(因为申请栈帧需要内存)
3.JAVA堆:所有线程共享 :
- 所有对象的实例以及数组都要在堆上分配内存。
由于收集器基本都是分代收集算法,所以java堆还可以细分为:新生代,老生代,可以处于物理不连续的存储空间。
4.方法区:所有线程共享:
- 存放虚拟机加载的类的信息,常量,静态变量等。
5.运行时常量池:方法区的一部分,
什么是对象:
- 对象包括:对象头,实例数据,对其填充。
- 对象创建:当new时,虚拟机回去检查常量池中是否有对应的类的引用并且是否其被加载,解析和初始化,对象需要的大小在类被加载后就可以确定了
- java虚拟机维护一个空闲的列表,记录堆上面那个区域是可以用的,
对象分配好内存后,要将对象对应的类的实例,元数据,对象的哈希码等存放在对象头。
引用
- 强引用:用 new 出来的
- 软引用:如果第一次发现内存不够,会将软引用对象放入二次回收中,如果还是内存不足,那就会抛出异常。
- 弱引用,下一次GC发生的时候,就会回收。
- 虚引用
回收
- 当一个对象不可达时,要被回收还要经过2次标记,第一次发现没有对应的GcRoots链后,标记一次,如果发现该对象重写了对应的finalize()方法,那虚拟机会将该对象放在一个F-queue队列里面标记,记住,finalize方法是不能做耗时操作。
当F-Queue队列被finalize线程执行时,会进行第二次标记,如果这个是finalize方法将该对象重新引用,那么该对象就会被标记为不是回收对象。
备注:finalize方法只会被虚拟机调用一次。
- 回收方法区(永久代):
方法区回收分为:废弃的常量和无用的类。
- 判断一个类是无用的:
1.内存中已经没有该类的任何实例,
2.加载该类的ClassLoader已经被回收。
3.该类对应的Class对象没有任何引用。
(永久代也会溢出的,大量使用反射,动态代理的时候,要适时卸载类)
2.回收算法
- 标记-清除
- 重新复制
- 标记-整理。
目前商用的虚拟机将java堆分为新生代和老生代,然后再根据不同的区域采用不同的回收算法。
一般对象都是分配在eden(新生代),当新生代内存不够时,会发生一次minorGC
- 需要大对象时,会在老生代分配内存,大对象,比如byte[]数组等
大对象对于内存分配不友好。更要避免分配临时的大对象,因为有可能在内存还很多的时候,还要触发GC来给大对象分配连续的内存,
当一个对象经过一段时间的回收后还存活,会将其移动到老生代。
以下况会触发GC,使用的算法是mark-sweep。
- 其中,Mark阶段从根集(Root Set)开始,递归地标记出当前所有被引用的对象,而Sweep阶段负责回收那些没有被引用的对象
- gc是虚拟机启动的时候创建的一个线程,每次在堆上面成功创建一个对象的时候,都会检测当前的闲堆大小是否小于等于128k,如果是的话,就会调用有关触发GC_CONCURRENT类型的GC。闲时会等待一定的时间,等待后如果发现没有被调用gc那么它就调用函数trimHeaps对Java堆进行裁剪,以便可以将堆上的一些没有使用到的内存交还给内核。。否则的话,就会调用函数dvmCollectGarbageInternal进行类型为GC_CONCURRENT的GC。
- 当应用程序调用System.gc、VMRuntime.gc接口,或者接收到SIGUSR1信号时,最终会调用到函数dvmCollectGarbage。