为什么需要JVM这种东西?像python那种程序不是更好,一边编一边运行。且运行要搞一个虚拟机来运行它呢?
首先要了解编译型语言和解释型语言的区别。
编译型语言是指将源代码一次性全部转化为二进制机器码来进行运行。
比如C语言。
解释型语言是一边执行一边转换,然后需要哪些人代码,哪些原代码就转化为机器码。
比如Python语言。
同时
编译型语言的缺点就是可移植性差、不灵活。
1、是原代码不能跨平台执行
2、是编译后的可执行文件也不能跨平台。
他的优点就是一次编译可以无限次运行。
解释型语言的缺点
1、一边执行一边转换,导致效率很低。
但他的优点是跨平台信号通过不同的解释器将相同的源代码,然后解释成不同平台下的机器码。
但是Java比较奇葩,Java是半编译型半解释型语言。原代码需要先转换成一种字节码文件在Java中,就是.class文件,然后再将这个文件然后在虚拟机中执行,Java中设计了这种机制,然后他的初衷就是在跨平台的基础上,然后提高执行效率。他其中的跨平台就是用.class文件,然后匹配不同的虚拟机就可以在不同的平台上运行。
所以为了实现跨平台。许多语言用的都会有虚拟机。java系的语言都会使用JVM,比如kotlin、scale。
JVM主要由类加载器、类运行时数据区、执行引擎和本地方法接口组成。
其中最重要的部分是运行时数据区。
里面包括:
其中,堆、方法区、栈这三个是重点
程序计数器
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
在栈中
但其实更多时候出问题都在堆中。
堆
其中Eden:S0:S1 = 8:1:1
老年代 :新生代 = 2:1
为什么是这个比例呢?
因为设计的人根据统计学规律得出了,这个参数是可以改的,但一般最好不要动。
对象有一个对象头,里面存了一个值代表年龄。
这个时候默认分到伊甸园区。
一段时间后,发现Eden区快满了, JVM有一个守护线程,叫做垃圾回收线程就出现了。
这个垃圾回收线程叫做MiniGC,他会通过垃圾回收算法(引用计数法),把所有存活对象引到S0区,然后这个对象的年龄加1 。每幸存一次,对象的年龄就会加1 。当他的年龄达到15的时候,就会进入老年代中。(一些比较大的对象,会直接进入老年区)
老年代慢了后,触发MajorGC,清理老年区对象,剩下的移到永久区。
问:清理垃圾的时候,还可以继续执行java程序吗?
答:当然不可以啦。不可以一边扫地一边扔垃圾呀。
专业术语叫stopworld(所以程序会有一个小小的停顿)
引用计数法就是把每个对象都统计一下,看一看这个对象有没有给其他代码或者对象引用到。引用到的话就记为1。否则记为0。如果为0的话,说明这个对象不太可能会被引用到,那么这个对象就是可回收对象。
缺点是:无法检测出循环引用,
如果有一个A对象和一个B对象,其中,A一直引用B对象,然后B引用A对象,那么他俩永远也无法回收。
也正因为这个缺点,所以 Java虚拟机不使用引用计数法。
把必要的一定会用到的对象当做根对象。然后看这个根对象会引用到哪些对象,后续我们只保留这些被引用到的对象,对其他对象进行回收。
所以这个算法的关键点就在于我们应该把哪些对象作为根节点对象GC Root。
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
一般来说,根对象包括:
以上两个算法是具体的分析哪对象是可回收的,哪个是不可回收的。他只是分析了哪些对象是要被回收的,但是没有具体的回收过程。
⾸先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。
但是他有一个非常显著的缺点就是空间问题。
就比如说你这个空间内存里头总共有100个对象,然后大概有98个都是不用的,但是这98个中间是分散的,所以当你把这98个删除完的时候,你可能第1个位置有一个需要的对象,第10个位置有一个需要的对象,最后又有一个需要的对象,然后导致它的空间被分割非常分散,会产生大量不连续的碎片。
它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使用的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进行回收。
用于前面说的Mini GC
根据⽼年代的特点提出的⼀种标记算法,标记过程仍然与 标记-清除 算法⼀样,但后续步骤不是
直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的对象。
用于上文提到的Major GC
根据对象存活周期的不同将内存分为几块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新⽣代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活⼏率是⽐较高的,而且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
新生代收集器
老年代收集器
新生代和老年代收集器
概述: Serial是一类用于新生代的单线程收集器,采用 复制算法 进行垃圾收集。Serial进行垃圾收集时,不仅只用一条单线程执行垃圾收集工作,它还在收集的同时,所用的用户必须暂停。
特点:
应用场景:适用于单核服务器、Client模式下的虚拟机。
优势:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
缺点:会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。
概述: parNew收集器其实就是Serial的一个多线程版本,其在单核cpu上的表现并不会比Serail收集器更好,在多核机器上,其默认开启的收集线程数与cpu数量相等。
除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
特点:
优点:随着cpu的有效利用,对于GC时系统资源的有效利用有好处。
缺点:和Serial是一样的。
适用场景:ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器。因为CMS收集器只能与serial或者parNew联合使用,在当下多核系统环境下,首选的是parNew与CMS配合。ParNew收集器也是使用CMS收集器后默认的新生代收集器。
概述: Parallel Scavenge也是一款用于新生代的多线程收集器,也是采用复制算法。
与ParNew的不同之处在于 Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,而ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间).
特点:
优点追求高吞吐量(运行代码的时间/垃圾回收的时间 的比值最高)。高效利用CPU,是吞吐量优先,且能进行精确控制。
Serial Old是Serial收集器的老年代版本。
特点:
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途(在后续中详细讲解···):
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
Serial / Serial Old收集器工作过程图(Serial收集器图示相同):
是Parallel Scavenge收集器的老年代版本。
特点:
应用场景:注重吞吐量与CPU资源敏感的场合,与Parallel Scavenge 收集器搭配使用,jdk7和jdk8默认使用该收集器作为老年代收集器。
一种以获取最短回收停顿时间为目标的收集器。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的缺点:
一款面向服务端应用的垃圾收集器。
特点如下:
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。