一、内存模型
方法区 线程共享区域,存储类信息、静态变量、常量等
堆 线程共享区域,存放对像实例信息
虚拟机栈 线程私有区域,存储每个线程调用方法的局部变量、操作数栈、出口等
本地方法栈 同虚拟机栈差不多,线程私有,不同的是它存储的是Native方法的局部变量、操作数栈、出口等信息
程序计数器 线程私有,存储的是线程下一条指令的地址,作用是控制系统按我们的编码顺序(不是写代码的顺序)去执行指令
在写代码的时候最常遇到的两种内存溢出异常,第一个是堆内存溢出(OOM),会产生OutOfMemoryError异常,一般是创建了大量的实例对像一直得不到释放导致的;第二个是栈内存溢出,会产生StackOverflowError,最常出现的场景是我们写了个递归一直不结束,或者是调用方法的时候笔误写成了调用自己(还是递归,只是本意不是想写成这样,粗心造成的),使得栈内存耗尽,不管是虚拟机栈还是本地方法栈,都是一样的。
二、GC
当内存不够用了,就会产生GC,但GC也分Minor GC 和 Full GC,会发生哪一种GC取决于申请的内存大小。
我们都知道jvm把内存模型的堆区分为年轻代和老年代,GC也是发生在堆区,所以需要注意的是像常量、静态变量以及我们加载的类信息都是放在方法区的,这部分的数据会一直存在直到系统停止,所以经常发现一些长年启动没有重启过的系统它占用的内存会越来越大,即使已经很少使用它上面的业务了。
年轻代还有细分,分成两块比较小且完全一样大小的from/to(有的地方叫法不同,都是指的同一个东西)区,和一块比较大的eden区,from/to区作用主要是辅助MinorGC,作第一次清除复制使用的。
当创建一个实例对像的时候,首先会去堆内存中申请需要的内存,如果年轻代里面(from或者to区域,看当前哪区域是非空白区域就选哪一个)有足够的内存,直接就分配了,不会触发GC,同时会把这个实例对像放到这个区域;如果不够,就会首先对from或者to 区域进行清理(清理的方式就是找出from或者to区域还在使用的对像,把它们复制到另一个备分区域,同时清空当区域;如果当前区域是from,就复制到to清空from,反之亦然),清除之后如果内存够了,直接分配,不够就对eden区进行清理,并把from或者to中的对像移进eden区,这个过程就是Minor GC。
如果清理完了eden区还是不够的话,这时就需要对老年区进行GC了(Full GC),过程还是一样的,把老年区不在使用的对像清理掉,把eden区存活对像移进来,空出from/to区存放新对像,如果空间足够,那么实例正常创建,还是没有足够空间,这时就会抛出OOM异常了。
当然了,触发GC操作的时候并非是年轻代或者老年代的空间完全占满才触发,实际上是有个阈值参数,比如达到70%就会触发,我们可以修改这个参数,绝大多数情况我们不修改这个参数,也不建议修改,所以只需知道有这个参数即可,不用记住;
年轻代和老年代使用的GC算法也是不同的,有参数可以让我们配置年轻代和老年代使用的GC算法,但需注意一点,这两种算法有的是有依赖的,不是说两两组合都可以;
算法主要用复制算法(如from/to)、标记清除算法、标记压缩/整理算法。
算法的实现有好几种,也有单线程的并发的,只需要了解它他们思路即可,需要的时候再查。
三、CMS
单独拿出来说因为比较重要,可能面试的时候会问到。
cms,并发标记清除算法,它是老年代的GC算法,步骤如下:
1、初始标记,单线程的,并且此时会停止应用的工作线程即stop the world,这时候标记的对像只是GC root 直接引用对像和年轻代存活对像直接引用的对像,并不会把这些对像引用的对像一个个遍历标记出来。
2、并发标记, 多线程的,遍历初始标记的对像,把它们引用的对像都标记上,此时因为应用程序也在运行,可能修改一些引用关系,所有这一步标记的对像可能会漏掉一些对像。
3、重新标记,多线程的,但此时又会暂停应用程序的工作线程;当在并发标记的步骤中,如果应用修改了一个对像的引用关系,这一块内存区域会被标记为脏块,当处于重新标记阶段时,就会对这些脏块里面的对像重新遍历他们的引用关系,标记存活对像
4、并发清理,多线程,不停止应用线程;需要注意的是cms不会去整理空间内存,会导致老年代内存碎片化,当内存过于碎片化的时候,因为分配一个新对像的时候需要连续的空间,就容易造成full gc,官方给的解决办法思路是加大老年代的内存大小,或者降低Gc的阈值使得jvm提前Gc,或者我们主动调用System.gc()提醒jvm GC,对应的调整参数可以百度。
注意到 cms虽说是并发Gc算法,但仍然需要暂停应用工作线程,不同的是,它把过程分得更细,虽然有两次暂停,但总的时间仍然是减少了,目前推荐的老年代算法是cms,但仍需考虑自己的业务场景(没有特殊需求不用管gc算法,默认就行了)。
面试常问的一个问题是如果判断一个对像是否存活?gc root 包含哪些对像?
1、判断对像是否存活的方就法是可达性分析,也就是gc root对像开始一个一个的找,找得到的就是可达的,存活的。
2、哪些对像要可以作为gc root? 首先想到的是方法区里面的对像,类、静态变量、常量等引用(final修饰的变量也是放在常量池中的,所有它是不会被GC的),第二个是虚拟机栈和本地方法栈中的变量等,这些变量属于正在运行的线程,有线程的引用,所以也作为GC root 对像。