参考这个博客:
https://mp.weixin.qq.com/s/4c9K5eYMFGVV2WyKaYXVBA
参考书籍:深入理解Java虚拟机
JVM就是java虚拟机的意思,java之所以可以跨平台,就是因为其语言运行在虚拟机上,从而达到跨平台的效果
目前常用的虚拟机有两个,一个是sun公司的hotspot,另一个是 JRockit
java的内存管理,其内存分为三大部分,栈,堆,方法区(又叫块,永久代Permanent Generation)
上面只是一个笼统的划分,细分如下:
1.方法区(Method Area)
2.堆(heap)
3.栈(stack)
3.1虚拟机栈,就是常说的对象变量存放的地方
3.2本地方法栈,为JVM中native方法服务
3.3程序计数器,就是程序运行时,运行到哪一步的一个记录器。多线程中程序切换,就是由各自线程的程序计算器来记录当前线程运行的状况,以便切换后能继续执行下去。
如下图:
栈:
栈是线程独有的,堆和块是线程共享的,所以栈的生命周期与线程相关,一般不用回收,只有堆和区需要
栈是存放对象的引用的,比如,Object j=new Object(); 那么这个j就是存放在栈中,还有局部变量等。
一般如果出现栈溢出(StackOverFlow),那么都是方法死循环造成的。
堆:
堆是存放对象的空间,上面这个j对应的内存空间就是在堆中,这部分才是我们可以控制的,也是gc(垃圾回收)需要注意的数组也是在堆上分配
堆如果细分,有新生代(年轻代),老年代(年老代),再细分,年轻代内存又被分成三部分,Eden空间(伊甸园)、From Survivor空间(S0)、To Survivor空间(S1),默认情况下年轻代按照8:1:1的比例来分配
方法区
方法区,是存放类信息,类的常量,静态变量,接口,方法等。
也可以称为永久代(Permant Gernation),一般不轻易回收,但也会,不是真的永久。。。
当对象没有被引用时,即j=null时,而且没有其它对象引用j,才会回收堆上的空间
JVM设置的值
-Xmx ——最大堆大小
-Xms ——初始堆大小
一般Xmx与Xms设置大小相同,不然当内存达到Xms时,未到Xmx,需要再分配内存时,会触发fullGc。而如果一开始就分了Xmx内存大小,则没有这个问题。但是会在内存不足时,直接报错(当机),没有再分配的机会。
-XX:PermSize ——方法区最小值
-XX:MaxPermSize——方法区最大值
-Xss——栈的大小, 每个线程可使用的内存大小
-Xmn——新生代大小,老生代=Xmx-Xmn,Xmn不能等于Xmx,否则会报错
-XX:NewSize——新生代初始大小
-XX:MaxNewSize——新生代最大值
一般用-Xmn代替上面两个设置。
-XX:NewRatio=m 老年代与 新生代比例 如果是5,则是5:1的意思
-XX:SurvivorRatio=m Eden和Survivor的比例
SurvivorRatio为新生代空间中的Eden区和救助空间Survivor区的大小比值,默认是32,也就是说Eden区是 Survivor区的32倍大小,要注意Survivo是有两个区的,因此Surivivor其实占整个young genertation的1/34
在设置内存时,可以用m,g等,但是不能m和g混着用,否则可能报错
32位系统,一般一个进程(java服务)分配的内存大小事有限的,比如linux给这个进程分配了2g内存,那么最大堆内存+最大方法区内存+栈内存不能大于这个2g,故一般设置时,Xmx,MaxPermSize最好不要大于服务器内存,否则会有可能服务无法启动
计数器很小一般忽略不计,栈内存一般默认1m,你可以调整。因为栈跟线程相关,故栈内存越小,能创建的线程就越多,那么线程数也不是无限的,操作系统一般有规定不能大于多少。
对象分配规则
对象被创建时,内存的分配首先发生在年轻代的Eden区(大对象可以直接在年老代),大部分对象在创建后很快就不再使用,很快就被年轻代的GC回收。
新生代分为伊甸园区(Eden),存活区1(S0),存活区2(S1),当伊甸园区满了之后,会执行一次垃圾回收(Minor GC),把剩余的放到两个存活区中。每经过1次Minor GC,对象的年龄计数器+1,到达阀值后,进入老年代。
对象如果在新生代存货了下来(几次GC后没被回收掉),则会被复制到老年代。年老代的空间比年轻代大,存放更多对象,发生GC的次数也少,当年老代内存不足,将执行full gc(整个内存区域垃圾回收)
垃圾回收
minorGc——一般在新生代,时间较短
MajorGC——一般在老年代,也可能因为minorGc后空间不足触发
fullGc——整个堆回收,时间较长,会导致整个应用停顿。比如你Xms=100m Xmx=1024,那么再内存不足时,由于没到最大值,会自动再给他分配内存,此时就会触发fullGc。或者你对象很大,老年代放不下,也会触发fullGc。如果到达1024,还要分配内存,就会出发内存错误。
对象存活判断:
1.引用计数
原理:对象有一个引用,则+1,删除一个引用,则-1,只收集计数为0的对象
缺点:无法处理循环引用的问题,如对象A和对象B互相引用,A.b=B,B.a=A,除此之外,没有其他对象引用A,B,那么无法回收他们
JVM并未使用此算法
2.可达性分析
GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
回收算法
复制(Copy)
原理:把内存空间划分为两个相等的区域,每次只使用一个区域。垃圾回收时,遍历当前区域,把存活的对象复制到另外一个区域, 再把已使用过的内存空间一次清理掉。
优点:不会出现碎片问题,高效,遍历一次即可。
缺点:需要两倍大的内存
应用场景:堆的新生代回收中使用。经过一次垃圾回收后的对象复制到Survior中
如图所示,可以看到,每次内存都只用了一半,很浪费。
标记 -清除算法(Mark-Sweep)
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象
缺点:会导致内存零碎,那么如果有大对象,将会出现没有连续的内存去存放这个大对象。需要两次遍历,第一标记,第二次清除。
如图所示,可看到回收后碎片较多。
标记—压缩
从GC roots开始遍历所有引用,对有活的对象进行标记。让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
优点:避免复制算法的空间问题,避免标记-清除的碎片问题
堆的老生代回收中使用该算法,因为要遍历,故比复制算法要慢
如图所示:
分代收集,Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
垃圾回收器
即使用回收算法的各种回收器,过于复杂,不讨论。有以下几种
Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
一般老年代比新生代大,这样可以在新生代触发minorgc,时间较短(影响较小),老年代空间大,就不用老是gc了。
如果gc时间较短,几百毫秒,基本不需要怎么调整,如果1秒以上,才需要关注。