1.对于JVM我们到底应该知道哪些
2016年6月份毕业至今,也开发过大大小小的项目,从单体应用到分布式项目,从springmvc到springboot,从繁琐的配置文件再到现在的开箱即用,感觉经历了技术的几次变革,但是扪心自问,我从来没有一次接触到过JVM,我不知道JVM对于我的开发来说意义到底在哪,每次想学,想了解一下JVM,感觉特别棘手,网上的博客一堆一堆的。但是每次都擦肩而过,感觉这东西不适用又很神圣,但是这是一个高级工程师必须要掌握的东西,了解底层,你就能掌控整个流程,这和业务无关,仅仅是技术的方向,你能感受到一个对象他具体存在哪个位置,一个变量从无到有最后又怎么消失,你能感受到你所用的Java到底牛逼在哪里,你能知道一个类文件是怎么加载的,又是如何相互不影响的,后台又是怎么“偷偷”的给你回收掉,你能感受到技术的魅力,GC经历过N次变革,从串行到并行,从并行到并发,收集能力提升了,但是我们是不知道的。一个机器可以到达最高多少并发,又是怎么计算的,你的程序是在最优秀的环境“驰骋”吗?对不起,我不知道,所以,我想了解一下JVM~
那就让我们来摸一摸它~
2.JVM RunTime Data Area (运行时数据区)
其实从这俩张图上应该可以知道,运行时数据区域可以划分为俩个部分,线程共享区域,和线程独占区域,这也就是出现线程不安全的主要原因
线程共享区域:方法区,堆
线程独占区域:程序计数器,虚拟机栈,本地方法栈
在把具体的每个含义解释一下,在深入一点。
共享区-方法区:左边的图上以及标记清楚了,说的在直白一点,就是所有类的相关信息都放在这里面,什么是类的相关信息,字段,方法,私有构造方法,类的常量等等。No-Heap 非堆内存,也就是下面说的永久代或者元空间
堆:堆主要作用是数据的存储,也就是垃圾回收关顾的地方(重要)。
(1)堆划分:新生代,养老代,永久代
(2)新生代:伊甸园区(Eden),From Survivor,To Survivor,这三块的比例为8:1:1
其中通过new()产生的对象都存在Eden园区,然后Eden区满了以后会触发GC进行垃圾回收,然后把剩余的对象存放在From Survivor中,如果From Survivor中也慢了的话,就把From Survivor中的对象放到To Survivor中去,这个地方存在一个逻辑的问题,就是谁空是谁是From Survivor,经历过15次垃圾回收才会被放入到养老区,在这之前触发的都是轻量级GC又称yongGC,如果养老区也满了的话,就会触发重量级GC也称FullGC。如果养老区fullGC以后依然腾不出空间的话就会出现OOM异常
From Survivor-->To Survivor
To Survivor-->From Survivor
(3)JDK1.8以后,永久代改名为元空间,并从堆内存中划分出去,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
程序计数器(PC寄存器):每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址),由存储引擎读取下一条指令,是一个非常小的存储单元,就是标记一个栈帧中方法执行到哪一行的一个标记,或者一个工具。
虚拟机栈:主管方法运行的,所有方法在运行时创建的局部变量都存储在栈帧中,栈是一种数据结构,先进后出的原则想必大家都知道,随线程创建的时候创建,随线程消失的时候消失,所以该部分也不存在垃圾回收。
本地方法栈:和虚拟机栈类似,只不过是运行的本地方法栈,native也就是C为Java提供的本地方法
3.垃圾回收算法
3.1 垃圾回收算法的种类:复制算法,标记清除算法,标记整理算法,分代收集算法
3.2 算法的优缺点:
复制算法(Copying):速度最快,但是会牺牲一部分内存。
标记清除(Mark-Swarp):虽然不会牺牲内存,但是会产生内存碎片。
标记整理(Mark-compact) :虽然不会产生内存碎片,但是牺牲性能。
分代收集算法:这个其实是前三个的综合,年轻代采用复制算法,牺牲掉一块Survivor区域,而年老代采用就是标记整理算法,永久代采用的是标记清除算法。
3.3 如何判定该对象是可回收对象(引用计数器和可达性分析)
引用计数器:该方法的缺点就是循环引用的问题无法解决
可达性分析算法:这个也是现在最常用的算法,难点在于如何判定GC ROOT 也就是跟节点
a.虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中常量引用的对象
d.本地方法栈中JNI(Native方法)引用的对象
4.垃圾回收器
一共有七种垃圾回收器:-XX:+PrintGCDetail(打印GC详情) -XX:+PrintCommendLineFlags(打印后缀信息)
如上图所示,新生代垃圾收集器和老年代的垃圾回收器是一一对应的,也就是说当我们指定其中一种垃圾回收期的时候,另一种也默认一同使用,但是G1例外。
Serial:新生代串行垃圾回收器,也称单一线程收集器,虽然效率最高,但是会出现"Stop-The-World",这句话就是说,在垃圾回收的时候所有用户线程均要停止,等待垃圾回收完毕。通过配置-XX:UseSerialGC来指定垃圾回收器,这时候,养老区默认指定SerialOld垃圾回收器。1:1
ParNew:新生代并行垃圾回收器,就就是从单一的线程变成多线程同时开始收集,但是避免不了出现"Stop-The-World"现象,对比单线程收集,并行速度会更快。如果新生代激活ParNew,老年代激活的是SerialOld垃圾收集器。通过配置-XX:UseParNewGC参数来指定。也是现在我们默认的垃圾回收器。但是在最后有标记,这种方式已经不被推荐使用,也就是说后续可能老年代变成CMS垃圾回收器。N:1
ParallelScavenge:新生代并行垃圾回收器,指定该垃圾回收器的同时,年老代同时会使用ParallelOld垃圾回收器,也就是说新生代和年老代同时并行收集,同样是并行收集器,ParallelScavenge比ParNew的牛逼的地方在于自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数已提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量,可以通过设置(-XX:ParallelGCThreads)指定垃圾回收的线程数量。N:N
SerialOld:(单线程的标记清除):Java8以后已经不推荐使用了。
CMS(并发标记清除) :是一种以获取最短回收停顿时间为目标的垃圾回收器,适合应用在互联网或者B/S系统的服务器上这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,CMS非常适合堆内存大,CPU核数多的服务器端应用。并发只的是和用户线程一起执行,只有在标记和重复标记的时候才需要低停顿,通过制定-XX:+UserConcMarkSweepGC,开启后新生代默认使用ParNew,当然同时SerialOld的收集器将作为CMS的备用垃圾回收器。
初始标记(initial-mark)-->并发标记和用户线程一起(Concurrent-mark)-->重新标记(remark)-->并发清除(concurrent-sweep)
G1垃圾回收器特点:
整理空闲空间更快
需要更多的时间来预测GC停顿时间
不希望牺牲大量的吞吐性能
不需要更大的JavaHeap
G1于CMS相比
G1是一个有整理内存过程的垃圾收集器,不会产生过多的内存碎片
G1的Stop-The-World更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间
G1主要改变的是Eden,Survivor,Tenured等内存区域不在是连续的,而是变成一个一个大小一样的region区域,每一个region从1M到32M不等,最多可以设置2048个区域,也就是说最大只能分配64G内存。
G1整体采用的是标记整理,局部采用的复制算法,虽然依然保留着新生代,年老代等概念,但物理内存上不在是连续的。
可设参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=n 设置G1区域的大小,值是2的n次方
-XX:MaxGCPauseMillis=n 最大GC停顿时间
-XX:InitatingHeapOccupancyPercent=n 堆占用多少的时候就触发GC 默认是45
-XX:ConcGCThreads=n 并发GC使用的线程数量
-XX:G1ReservePercent=n设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认是10%
6.内存调优(常用工具?具体场景)
这里就埋一个伏笔吧~~~~别失望,如果有时间或者有好的机会,我会把这块补充上。
好了,写到这里,感觉头发都掉了不少了,以上仅仅代表个人观点,我以一个小白的角度对JVM的初识,如果有大佬看到小弟的失误,欢迎来指正~