文章目录:
垃圾回收算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现。Java规范中并没有对垃圾收集器的实现有任何规范,所以不用的厂商、不同的版本的虚拟机提供的垃圾收集器是不同的,这里主要讨论的是HotSpot虚拟机所包含的虚拟机,按照年代划分如下:
其中新生代收集器有Serial、ParNew、Parallel,老年代收集器有CMS、Serial Old、Parallel Ol,G1则既可以在新生代收集,又能在老年代收集。两个垃圾收集器之间如果存在连线,则说明它们可以搭配使用。
那哪个收集器的性能最好呢,其实这里并不存在最好的收集器,只有在对应场景中最合适的垃圾收集器
Serial收集器是最基本的收集器,并且是单线程的收集器,这里的单线程不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,在它进行垃圾收集时,必须暂停其他所有的线程工作,直到它收集结束。不难想象,这对很多应用来说都是难以接受的,如下图
除了上面写到的缺点,Serial收集器也有着优于其他收集器的地方,简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程的收集效率。
ParNew收集器是Serial收集器的多线程版本,除了使用多条线程进行垃圾回收外,其他地方与Serial一样,从下图中也可以看出,除了多了几个GC线程,和Serial收集器并没有什么区别
Parallel Scavenge 是一个使用标记-复制算法的多线程收集器,看起来和ParNew很像,Parallel Scavenge收集器的关注点和与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间(用户体验),而Parallel Scavenge收集器的关注点是达到一个可控制的吞吐量(提高CPU的效率),这里的吞吐量指的是CPU用于运行代码的时间和CPU总消耗时间的比值。
那更短的停顿时间和更高的吞吐量有什么好处呢?
停顿时间越短越适合需要与用户交互的程序,良好的响应速度可以提升用户体验。更高的吞吐量适合在后台运算而不需要太多交互的程序,高吞吐量可以提高CPU的利用率,尽快地完成程序的运算任务。
Serial Old是Serial收集器的老年代版本,同样是单线程收集器,采用标记-整理算法,主要有两大用途:一是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,二是作为CMS收集器的后备预案。
Parallel Old是Parallel Scavenge收集器的老年代版本,采用多线程和标记-整理算法。该收集器是在JDK1.6才开始提供的,因为当新生代选择了Parallel Scavenge收集器,老年代只能选择Serial Old(Parallel Scavenge无法与CMS搭配使用),这时Serial Old收集器会影响整体的吞吐量,所以提供了Parallel Old收集器和Parallel Scavenge搭配使用
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,采用标记-清除算法,其运作过程可以分为初始标记、并发标记、重新标记、并发清除四个步骤。
上述四个步骤中,初始标记和重新标记两个步骤会“Stop The Word”,也就是会暂停用户线程,如下图
这里解释下在垃圾收集器的语境中,并行和并发的概念:
CMS的优点是并发收集,停顿时间短,缺点主要有以下三个:
G1收集器是面向服务端应用的垃圾收集器,回收范围包括新生代和老年代,主要有以下特点:
G1收集器的运作步骤如下:
看起来和CMS很像,如下图
先解释下什么是类加载机制,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。其实在看类加载之前最好了解下类文件结构,因为面试中这部分问的不多就不介绍了。
类从被加载到虚拟机内存中开始,到卸载出内存为止,生命周期包括:加载、验证、准备、解析、初始化、使用和卸载等7个部分,其中验证、准备、解析统称为连接。
类的加载过程也就是类的生命周期的前五部分,加载、验证、准备、解析、初始化
在加载部分,虚拟机需要完成以下三件事:
验证的目的是为了确保Classw文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。验证主要分为四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区分配。注意:这时候进行内存分配的只有类变量,不包括实例变量,其次,这里指的初始值一般是数据类型的零值,public static int x = 1;,变量x在准备阶段被设置的初始值是0而不是1,而程序被编译之后,x的值才为1。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时可以无歧义定位到目标即可。直接引用是指直接指向目标的指针、相对偏移量或是一个可以直接定位到目标的句柄。
看了上面符号引用和直接引用的概念,相信很多人还是一头雾水,那符号引用转为直接引用有什么用呢?
其实符号引用就是通过一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定加载内存中,这时虚拟机并不知道对象的内存地址,所以光有符号引用是不够的。而直接引用是可以指向目标的指针,是和虚拟机实现的内存布局相关的,也就是可以确定对象在内存中的位置的。
简单来说就是在编译的时候,类会编译成一个class文件,但在编译的时候虚拟机并不直到知道所引用类的地址,这时就用符号引用来替代了,在解析时将符号引用转为直接引用就是因为直接引用可以找到类在内存中的地址。
初始化是类加载的最后一步,也是类加载的最后一步,从这开始JVM开始真正执行类中定义的Java代码
前面介绍了类的加载过程,类加载器的意义很容易理解,作用就是将类加载到虚拟机的内存中。
这里再提个挺重要的知识点,任何类都需要加载它的类和加载器和这个类本身确定其在Java虚拟机中的唯一性,也就是说要比较两个类是否相等,只有两个类由同一个类加载器加载的前提下才有意义,如果两个类不是由同一个类加载器加载,那么它一定不相等。
JVM主要提供三个类加载器:
双亲委派机制是面试中非常高频的一个知识点,需要牢牢掌握
双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是这样,所以所有的加载请求最终都应该传送到顶层的启动类加载容器,只有当父类加载器无法完成加载时,子加载器才会尝试自己去加载,如下图
既然问了,那肯定就不是了,在双亲委派模型中,类加载器之间的父子关系一般不是以继承关系实现的,而是组合的关系来复用父加载器的代码的
双亲委派的保证了Java程序稳定地运行,可以避免类地重复加载(父类加载器加载过,子加载器不会再进行加载),保证Java的核心API不被篡改,例如,你自己编写了一个java.lang.Object类,也不会被加载,因为根据双亲委派机制,会由启动类加载器进行加载,会先加载位于rt.jar中的java.lang.Object类,并且其他子类加载器不会再去加载ava.lang.Object类。
从上面的介绍可以看到父类加载器的优先级是大于子类加载器的,只有父类加载器无法加载,子类加载器才会去尝试加载,这在大多数情况是没有问题的,因为越上层加载的类通常是基础类(像Object类),一般情况这些基础类都是被用户代码所调用的API,但基础类要是想调用用户的代码,那就会出问题了,因为第三方的类不能被启动类加载器加载。
举个很经典的例子,JDBC服务在Java开发中非常常见(操作数据库),
Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "lurenzhang", "666");
DriverManager类是在java.sql包中,java.sql包的位置是jdk\jre\lib\rt.jar,也就是DriverManager类会先被启动类加载器加载,类在加载时其中有这样一段代码
ServiceLoader
这会尝试加载classpath下面的所有实现了Driver接口的实现类,而实现了Driver接口的第三方类库应由应用类加载器加载,这样一来启动类加载器加载的类使用了启动类加载器加载的类,违背双亲委派机制的原理。
这个需要先去了解双亲委派是怎么实现的,看下java.lang.ClassLoader的loadClass()源码就知道了,这里就不展开写了,想破环双亲委派自定义一个类加载器,重写其中的loadClass()方法即可。
原文出处:https://mp.weixin.qq.com/s?__biz=MzA4NjU1MzA2MQ==&mid=2647726128&idx=1&sn=64b1f854ad6aa2ae62bddd5343e495a6&chksm=87e340bab094c9ac51f046e2b8ad1cca46b661fc2d74b3ed398ed003f66c384a8dc4b7bf82c9&scene=178&cur_album_id=1966226418825035778#rd
留言与评论(共有 0 条评论) “” |