结构
JVM的内存是由虚拟机栈(VMStack)、本地方法栈(NativeMethodStack)、方法区(MethodArea)、堆(Heap)、程序计算器(ProgramCounterRegister)构成。
虚拟机栈:
在程序的使用中创建一个线程就会给他分配一个栈,栈中又存在栈帧,一个栈帧对应的是一个方法,当线程调用方法时此方法的栈帧就会入栈,方法执行完成则会出栈,并且栈帧执行的是一个先入后出的过程,最先入栈的栈帧最后出栈。
(异常):栈中存在两种异常
1、StackOverflowError:栈深度大于虚拟机所允许的深度。因为栈帧先入后出的原因,栈的栈帧内存是有限的,当栈帧数量超过栈的最大栈帧限制时则报出StackOverflowError异常
2、OutOfMemory:简单的说就是栈中存储的内容超出了栈的容量并且还无法扩容(当前大部分Java虚拟机都可以动态扩展,Java虚拟机规范中的也允许固定长度的虚拟机栈)
(线程私有):上文中说到一个线程创建的时候分配给它一个栈,所以也可以理解为栈是线程私有的
(GC):栈的生命周期因为绑定在线程上,当线程执行结束后内存自动释放,所以在栈中不会进行GC
本地方法栈:
本地方法栈运行的是本地(native)方法,本地方法栈与虚拟机栈运行的原理非常相似,只不过JAVA栈是为执行JAVA服务,本地方法栈是为执行本地服务的,甚至在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
(异常):栈中存在两种异常
1、StackOverflowError:栈深度大于虚拟机所允许的深度。因为栈帧先入后出的原因,栈的栈帧内存是有限的,当栈帧数量超过栈的最大栈帧限制时则报出StackOverflowError异常
2、OutOfMemory:简单的说就是栈中存储的内容超出了栈的容量并且还无法扩容(当前大部分Java虚拟机都可以动态扩展,Java虚拟机规范中的也允许固定长度的虚拟机栈)
(线程私有):上文中说到一个线程创建的时候分配给它一个栈,所以也可以理解为栈是线程私有的
(GC):栈的生命周期因为绑定在线程上,当线程执行结束后内存自动释放,所以在栈中不会进行GC
方法区:
方法区有的人叫永生代或者非堆,方法区存储的内容在java版本中是不一样的,在java1.7之前是方法区是在堆中的,可以理解为堆的逻辑部分,主要存储常量、静态变量、类信息、编译后的代码,在1.7中将方法区从堆中拆分了出来常量池和静态变量存储到了堆中,存储内容保留了类信息和编译代码,而在1.8版本方法区变为元空间,存储内容没有改变,元空间是存储在本地(native)的
(异常): OutOfMemory:存储内容超限并且内存无法扩展时
(线程共有):方法区存储的是常量和全局范围的静态变量、类信息、编译代码等
(GC):方法区因为是线程共有也会出现GC(垃圾回收)的情况
(去除永生代的问题)
(1)字符串存在永久代中,容易出现性能问题和内存溢出。
(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
堆:
堆是JVM中占用内存最大的一块区域,它一般存储对象和数组,因为线程共有所以在对对象内存分配的时候需要加锁,但是所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),在TLAB分配对象不需要加锁,所以分配对象时尽量在TLAB上分配,当对象过大时在堆上分配
(异常): OutOfMemory:存储内容超限并且内存无法扩展时
(线程共有):所有new对象和数组都存储在堆上
(GC):方法区因为是线程共有也会出现GC(垃圾回收)的情况,因为堆的线程共享及存储内容类型,堆就成为了GC的主要处理区,同时因为内存较大存储内容较多,堆还分为了年轻代和年老代,年轻代中有eden、From Survivor(s0)、To Survivor(s1),他们的处理情况为新创建的对象将存储在eden区中,通过GC的算法判断对象是不是无用垃圾,如果是则清楚,不是则计数加1到From Survivor区,年老代的存储是满足特点条件,比如经历多次GC还存活的对象,一般为15次,或者内存过大,在进行GC处理后的年轻代(GC处理垃圾腾出空间)无法存储则直接放入年老代,若年老代还是内存不足则报出OutOfMemory异常
程序计算器:
程序计算器的占用内存特别的小,每个线程都有一个独立的程序计算器,线程私有,他是JVM中唯一不会出现OutOfMemory一次的地方
(异常): 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
(线程私有):每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
(GC):生命周期因为绑定在线程上,当线程执行结束后内存自动释放,所以不会进行GC
GC:
GC垃圾回收机制,上文中JVM中的五大区域里虚拟机栈,本地方法栈,程序计算器是线程私有的,生命周期与线程相同,当线程执行完毕内存会自动回收,所以不需要过多考虑GC,而方法区和堆是线程共享,内存的分配和回收是动态的,是GC的主要关注部分
(判断对象是否回收)
1、引用计数法
引用计数法是垃圾收集器早期的使用策略,堆中每个对象都有一个引用计数,对象创建的时候程序计算器给这个对象一个变量设置为1,当每调用一次这个对象则+1超过一定生命周期或者重新定义则-1,当为0的时候则可以被当作垃圾进行回收,当它被回收后,它引用的所有对象实例-1
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
2、可达性分析算法
GC Roots,垃圾收集的起点,我们可以这样理解,GC Roots是一个解药,当一个对象到 GC Roots 没有任何引用链相连(GC Roots 到这个对象不可达)时,就说明此对象是不可用的,是死对象,将会执行清除。
3、判断类是否废弃的条件
(1) 该类所有的实例已经被回收(堆中不存在任何该类的实例)。
(2)加载该类的 ClassLoader
已经被回收。
(3)该类对应的 java.lang.Class
对象在任何地方没有被引用(无法通过反射访问该类的方法)。
(回收算法)
(1)标记-清除算法:分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
(2)复制算法:把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环,实际可使用的内存空间缩小为原来的一半比较适合。
(3)标记-整理算法:先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存
(4)分代收集算法:把堆内存分为新生代和老年代,新生代又分为 Eden 区、From Survivor 和 To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。(现在大部分都用分代手机算法进行垃圾回收)
//不管使用什么算法,当进行垃圾回收的时候都会停止所有线程以等待垃圾回收完成, 所以尽可能的避免频繁的GC以减少对程序运行的影响