3.3 JVM
1.什么是JVM?为什么Java被称作是“平台无关的编程语言”?
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成字节码文件后在虚拟机上运行,它连接操作系统和Java程序。
正是因为有这样一层操作系统与程序之间的连接,Java程序在一台机子上编译后就可以在不同系统上的Java虚拟机上运行。因此Java被称为“平台无关”。原因是JVM知道底层硬件平台的指令长度和其他特性。
3.3.1 JVM的组成结构
由三个主要的子系统组成
- 类加载子系统
- 运行时数据区(内存结构)
- 执行引擎
![image-20200726223227779](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200726223227779.png)
3.3.2 类加载子系统
1. 类的生命周期
![image-20200726231012000](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200726231012000.png)
-
1.加载:将.class文件从磁盘读到内存中
- 通过全限定类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的.class对象,作为方法区这些数据的访问入口。
实际上是将.class的类型信息:类的版本、字段、方法、接口、修饰符、静态变量(类变量)加载到方法区
-
2.连接:与JVM进行连接
- 验证:class文件是符合jvm规范的
- 准备:给类变量分配内存,并赋予默认值( 如static int a = 1;此时给int分配内存4byte,默认赋予初始值a = 0)
- 解析:当前类还引用了其它类,所以本类加载器继续加载引用到的类,套娃的过程。(静态链接)
-
3.初始化:为类的静态代码块赋予真正的初始值,就是程序编写的初始值。static int a = 1;此时a=1
上面三个步骤:加载、连接、初始化,也就是类加载的过程。
- 4.使用
- 5.卸载(gc)
类装载方式,有两种
(1)隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
(2)显式装载,通过class.forname()等方法,显式加载需要的类 ,隐式加载与显式加载,两者本质是一样的。Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
2. 类加载器的种类
![image-20200727005944872](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200727005944872.png)
类加载器主要分为
**1.启动类加载器(由C语言实现)和2.其它类加载器(由Java实现且全部继承自java.lang.ClassLoader)**两种:功能就是各自负责找到不同包路径下的类!
- 1.启动类加载器:BootStrap ClassLoader(C语言实现的,负责加载jre的核心类库,%JAVA_HOME%/lib)
- 2.扩展类加载器:Extension ClassLoader(负责加载jre中扩展目录ext中jar类包,%JRE_HOME%/lib/ext)
- 3.系统类加载器:Application ClassLoader(负责加载classpath路径下的类包,加载用户当前写的类)
- 4.用户自定义类加载器:User ClassLoader(负责加载用户自定义路径下的类包)
a.由类加载器,去找到我们要加载的类
b.Java如何确定一个类是唯一的?通过《类的全限定类名+类加载器》来确定
3. 类的加载机制
- 双亲委派加载机制
问题:讲一下双亲委派模型?
每一个类都对应一个类加载器,系统的各个类加载器在协同工作时,使用的就是双亲委派模型加载机制:
1.向父类加载器提交加载请求同时验证是否被加载过,首先会把该请求委派给父类加载器的classLoader()方法处理 ApplicationClassLoader–>ExcentionClassLoader–>BootStrapClassLoader,
向上的过程判断该类是否被加载过
,若被加载过,则返回加载请求;因此未被加载的类的加载请求最终都会到顶层的(BootStrap)启动类加载器中处理2.从顶层的父类:启动类加载器开始向下找到适合当前类的加载器,来处理类加载请求。如下图所示
![image-20200727003301190](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200727003301190.png)
问题:双亲委派机制的优点?
1.避免类的重复加载。向上委派给父类加载器的过程中,判断类是否被加载过
2.防止开发人员篡改核心类库。比如说开发人员在工程中手动建立了一个java.lang.String类,那么双亲委派模型最后加载的应该是jdk的String类。
问题:如何避免使用双亲委派机制?
1.自己自定义一个类加载器,继承java.lang.ClassLoader然后重写loadClass()方法;
2.Tomcat中就打破了双亲委派机制,Tomcat中的各个war包运行在同一个jvm中,为了程序的正确运行需要隔离各个war包中类的加载和运行。所以打破了双亲委派机制的类加载形式。通知自定义类加载器,加载制定路径下的类,但是不会打破双亲的安全机制。核心类库API不会被用户写的同名类顶替执行。
- 全盘负责委托机制
一个类由某个类加载器加载时,该类所依赖和引用的类也由当前类的类加载器载入,除非你显示的申明使用某个类加载器
3.3.3 JVM的内存结构(运行时数据区)
Java程序执行流程
.java源文件–>被编译器编译成.class字节码文件–>由类加载器加载完毕后进内存,交给JVM执行引擎执行–> JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
![image-20200612104657949](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200612104657949.png)
1. 方法区
方法区?元空间?永久代?
方法区
是Java虚拟机的一种规范,永久代
是Hotspot虚拟机对这种规范的一个实现,受虚拟机内存限制。JDK1.8后元空间是直接作用与系统内存的,不受JVM内存限制。
问题:方法区的作用?
类加载器将某类的.class中的信息提取出来,将1.类型信息(类的版本、字段、方法、接口、修饰符)和2.类变量(static int a = 1;)存储到方法区中。
问题:运行时常量池在方法区?
JVM为每个已加载的类维护一个常量池,在jdk1.8以后,方法区的实现是元空间,作用与直接内存,运行时常量池仍在方法区,而字符串常量池被迁移到了堆内存中。
![image-20200727143153995](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200727143153995.png)
2. 堆
![image-20200727140424272](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200727140424272.png)
-
年轻代
-
Eden区:新建的对象大部分是新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中
- 最新new出来的对象,存放在Eden区;
- Eden区满的时候,执行引擎会调用minior GC来收集垃圾(比如一个对象GC Roots没有任何引用链的话,则此对象不可用,被回收。)
- 但是,比如线程池对象需要保留不应该被回收,则被移动到From区。
-
Survivor区
- From
- 移动到From区的对象,在From区满的时候,无用的被minior GC,有用的则被移到To区域,年龄加一。
- To
- 同理,To区也在满的时候执行minior GC,有用的对象,则被移回到From区域,年龄加一;当年龄满15之后,则被移动到老年代中去。
- From
-
-
老年代:1.用于存放新生代中经过多次垃圾回收后依然存活的对象。2.老年代放满了之后(线性增长)OOM,执行引擎执行full GC(这个过程耗时,影响性能,JVM调优,主要就是指这个过程)
![image-20200727155216247](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200727155216247.png)
问题:什么是堆:Heap?
是Java虚拟机管理的内存的最大的一块,Java堆是被所有程序共享的一块内存区域,在虚拟机启动时创建,唯一目的:存放对象实例,几乎所有的对象实例都在这里分配内存。(线程共享)
问题:在堆中创建对象的过程?
1.(JVM发现该类的信息未被加载到方法区时,先加载该类的信息到方法区中)
2.JVM为类创建实例对象时,首先得去方法区中查找该类的信息,确定在堆中为该实例分配多少内存?
3.如何在堆中划分这么多内存给该对象呢?有两种方式。(
指针碰撞和空闲列表
)4.调用构造方法初始化实例。该实例拥有指向该类在方法区的类型信息(包括方法表,动态绑定的实现原理?)的指针。
![image-20200727143858624](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200727143858624.png)
堆中如何进行垃圾回收?
1.有MinorGC回收年轻代中的内存
2.FullGC回收Heap和方法区中的内存(是MinorGC执行时间的10倍,stop the world,停掉所有用户线程,执行垃圾回收线程)。
问:什么对象会进入老年代?
对象头中有记录gc年龄的字段超过15,空间担保、动态年龄判断、大对象
问题:内存泄露 ?
1、用完不释放 2、GC回收速度赶不上使用的速度
GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
(1)Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法:
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代**。**但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。(2)Full GC 是发生在全局的垃圾收集动作(针对新生代、老年代、元空间)
堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长**。另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片** ( 即不连续的内存空间 )。此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
3. 虚拟机栈
虚拟机栈:线程私有,是每一个方法执行的内存模型,由一个个栈帧组成。每个栈帧又由《局部变量表、操作数栈、动态链接、方法出口》4个部分组成。
JVM有几个虚拟机栈 ?一个线程一个虚拟机栈
一个虚拟机栈有几个栈帧 ? 方法调用次数。
特别说明:java类中所有public和protected的实例方法都采用动态绑定机制,所有私有方法、静态方法、构造器及初始化方法都是采用静态绑定机制。而使用动态绑定机制的时候会用到方法表,静态绑定时并不会用到。
![JVM栈](/Users/caowei/Desktop/黑马程序猿/JVM/JVM栈.png)
问题:栈帧的组成?每一次的方法调用都创建一个栈帧并压栈
局部变量表(存放局部变量int a = 1;或者对象的引用地址Math a,指向堆中对象new Math())
- 局部变量表存放了基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配。
- 静态方法和实例方法对应的局部变量表基本类似,区别在于实例方法的变量表中,第一个位置存放的是当前对象的引用。
操作数栈(从局部变量表取值做运算,中转后存回局部变量表中):Java没有寄存器,所有参数传递都是使用操作数栈
动态链接:实例对象执行某方法时,会去方法区寻找该类的相关信息。
方法出口:方法的调用返回。
4. 本地方法栈
和虚拟机栈类似,用户执行本地方法Native的栈区空间,也会出现StackOverFlowError和OutOfMemoryError两种异常。
5. 程序计数器
程序计数器的作用?
1.字节码解释器通过改变程序计数器的值来依次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、线程恢复。
2.多线程的情况下,程序计数器用户记录线程上次执行到的位置,所以是线程私有的。
3.它是唯一一个不会出现OOM的内存区域,生命周期同线程的创建和结束。
执行引擎:1.字节码解释器:字节码–》c+±->机器码 2.模板解释器:Java字节码-》机器码
3.4 垃圾收集算法(垃圾收集器)
JavaGuide面试指南2.0
![image-20200727194949403](/Users/caowei/Library/Application%20Support/typora-user-images/image-20200727194949403.png)
3.4.1 垃圾收集有哪些算法
-
标记-清除算法:
标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。(产生内存碎片)
那么哪些对象可以作为GCRoots
-
复制算法:
与标记-清除算法相比,复制算法是一种相对高效的回收方法不适用于存活对象较多的场合,如老年代将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
-
标记-整理算法:
标记-整理(压缩)算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
-
增量算法
增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
3.4.2 有哪些常见的垃圾收集器
(1) Serial收集器-标记整理(压缩)
Serial收集器
是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即stop the world。到现在为止,它依然是虚拟机运行在client模式下的默认新生代收集器,与其他收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。
Serial Old
是Serial收集器的老年代版本,它同样是一个单线程收集器,使用”标记-整理“算法。这个收集器的主要意义也是被Client模式下的虚拟机使用。在Server模式下,它主要还有两大用途:一个是在JDK1.5及以前的版本中与Parallel Scanvenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。
通过指定-UseSerialGC参数,使用Serial + Serial Old的串行收集器组合进行内存回收。
(2)ParNew收集器
ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程
进行垃圾回收
(3)Parallel Scavenge收集器
Parallel Scavenge收集器是采用复制算法
的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
- (Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程和”标记-整理”算法。这个收集器是在jdk1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外别无选择。由于单线程的老年代Serial Old收集器在服务端应用性能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合”给力“。直到Parallel Old收集器出现后,”吞吐量优先“收集器终于有了比较名副其实的应用祝贺,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。)
(4)CMS收集器 - (标记清除算法)
CMS:Concurrent Mark Sweep:并发、标记清除
CMS(Concurrent Mark Swep)收集器是一个比较重要的回收器,CMS一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。从名字(Mark Swep)就可以看出,CMS收集器是基于标记-清除
算法实现的。它的收集过程分为四个步骤:
-
初始标记(initial mark):stop the word ,GC线程去记录与ROOT相连的对象。
-
并发标记(concurrent mark):GC线程和用户线程同时执行,用一个闭包结构记录所有可达的对象。
-
重新标记(remark):是为了修正上一阶段,用户程序运行导致变动的标记对象。
-
并发清除(concurrent sweep):用户线程和GC一起执行,GC线程清除不可达对象的空间。
注意初始标记和重新标记还是会stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。
(5)G1收集器
G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比
,G1具备如下特点:
并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。
空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。
可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
垃圾回收器的原理,可以主动通知虚拟机进行垃圾回收吗?
(1)对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。
(2)程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
问题:哪些对象可以作为GCRoots?