# JVM相关

1、Java 为什么能一次编写,处处运行?

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是何种语言。

Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大。因此,有一些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,这也为其他语言实现一些有别于Java的语言特性提供了基础。

总结:

  • 虚拟机不绑定java语言

  • 虚拟机并不关心Class的来源是何种语言

  • 字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大

2、JVM 是什么?

JVM是Java Virtual Machine(Java虚拟机)的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。

二、JRE、JDK和JVM的关系

**JRE(Java Runtime Environment, Java运行环境)**是Java平台,所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。

**JDK(Java Development Kit,Java开发工具包)**是用来编译、调试Java程序的开发工具包。包括Java工具(javac/java/jdb等)和Java基础的类库 java API。

**JVM(Java Virtual Machine, Java虚拟机)**是JRE的一部分。JVM主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。Java语言是跨平台运行的,不同的操作系统会有不同的JVM映射规则,使之与操作系统无关,完成跨平台性

JRE,JDK,JVM关系

image-20230523212953512

总结:使用JDK(调用JAVA API)开发JAVA程序后,通过JDK中的编译程序(javac)将Java程序编译为Java字节码,在JRE上运行这些字节码,JVM会解析并映射到真实操作系统的CPU指令集和OS的系统调用。

3、HotSpot 是什么?

提起HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。

SUN的JDK版本从1.3.1开始运用HotSpot虚拟机, 2006年底开源,主要使用C++实现,JNI接口部分用C实现。

Java原先是把源代码编译为字节码在虚拟机执行,这样执行速度较慢。而HotSpot将常用的部分代码编译为本地(原生,native)代码,这样显着提高了性能。

HotSpot基础知识

HotSpot包括一个解释器和两个编译器(client 和 server,二选一的),解释与编译混合执行模式,默认启动解释执行。

编译器:java源代码被编译器编译成class文件(字节码),java字节码在运行时可以被动态编译(JIT)成本地代码(前提是解释与编译混合执行模式且虚拟机不是刚启动时)。

解释器: 解释器用来解释class文件(字节码),java是解释语言(书上这么说的)。

4、JVM 内存区域分类哪些?

JVM (opens new window) 内存区域有:堆和栈,这是一种广泛的分法,也是一种按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享。

线程不共享区域

每个线程的数据区域包括:程序计数器、虚拟机栈、本地方法,它们都是在新线程创建时才创建的。

程序计数器(Program Counter Rerister)

程序计数器区域是一块较小的区域,它用于存储线程的每个执行指令,每个线程都有自己的程序计数器,此区域不会有内存溢出的情况。

虚拟机栈(VM Stack)

虚拟机栈描述的是 Java 方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

本地方法栈(Native Method Stack )

本地方法栈用于支持本地方法(native 标识的方法,即非 java 语言实现的方法)。

虚拟机栈和本地方法栈,当线程请求分配的栈容量超过 JVM 允许的最大容量时抛出 StackOverflowError 异常。栈溢出

线程共享区域

线程共享区域包含:堆和方法区。

堆(Heap)

堆是最常处理的区域,它存储 JVM 启动时创建的数组和对象,JVM 垃圾回收也主要是在堆上面工作。

如果实际所需的堆超过了自动内存管理系统能提供的最大容量时 OutOfMemoryError 异常。

方法区(Method Area)

方法区是可提供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。

当创建类和接口时,如果构造运行时常量池所需的内存空间超过了方法区方法区所能提供的最大内存空间就会抛出 OutOfMemoryError 异常。

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分, 每一个运行时常量池都分配在 JVM 的方法区中,在类和接口被加载到 JVM 后,对应的运行时常量池就被创建。运行时常量池是每一个类和接口的常量池(Constant_Pool)的运行时表现形式,它包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。

如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常。

JVM五大区域

  1. 程序计数器(Program Counter Register):(PC寄存器即程序计数器 ) 当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;(私有)

  2. Java 虚拟机栈(栈)(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;(私有)

  3. 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;(私有)

  4. Java堆((堆)Java Heap):Java虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;(共享)

  5. 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量】即时编译后的代码等数据。(共享)

5、堆和栈区别是什么?

  • 栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。它与数据机构中的堆是两回事,分配方式类似于链表。
  • 全局区(静态区)(static):全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域。程序结束后由系统释放。
  • 文字常量区:常量字符串就是放在这里。程序结束后由系统释放。
  • 程序代码区:存放函数体的二进制代码

堆和栈的区别

1申请方式

  • 栈:由系统自动分配。例如在声明函数的一个局部变量int b,系统自动在栈中为b开辟空间。
  • 堆:需要程序员自己申请,并指明大小,在C中用malloc函数;在C++中用new运算符。

2申请后系统的响应

  • 栈:只要栈的剩余空间大于所申请的空间系统将为程序提供内存,否则将报异常提示栈溢出。
  • 堆:操作系统有一个记录空间内存地址的链表,当系统收到程序的申请时,会遍历链表,寻找第一个空间大于所申请空间的堆节点,然后将节点从内存空闲节点链表中删除,并将该节点的空间分配给程序。对于大多数操作系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的对节点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入到链表中。

3申请大小的限制

  • 栈:在Windows下,栈是向低地址拓展的数据结构,是一块连续的内存的区域。站定地址和栈的大小是系统预先规定好的,如果申请的内存空间超过栈的剩余空间,将提示栈溢出
  • 堆:堆是向高地址拓展的内存结构,是不连续的内存区域。是系统用链表存储空闲内存地址的,不连续

4申请效率的比较

  • 栈:由系统自动分配,速度较快。但程序员无法控制。
  • 堆:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。 拓展:在Windows操作系统中,最好的方式使用VirtualAlloc分配内存。不是在堆,不是在栈,而是在内存空间中保留一块内存,虽然用起来不方便,但是速度快,也很灵活。

6、JVM 哪块内存区别不会发生内存溢出?

JVM 哪块内存区域不会发生内存溢出? 程序计数器(Program Counter Rerister)程序计数器是一块内存较小的区域,它用于存储线程的每个执行指令,每个线程都有自己的程序计数器,此区域不会有内存溢出的情况。

7、什么情况下会发生栈内存溢出?

栈是先进后出的数据模型,这里指的是运行时栈,主要是进行指令存储和辅助运行。栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型。 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError异常,方法递归调用产生这种结果。 如果JVM可以动态扩展,并且扩展动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建线程的时候没有足够的内存去创建对应的JVM Stack,那么JVM将抛出一个OutOfMemory异常(这是启动过多) 主要时候在进行递归的时候,在进行递归时,未完成不会释放资源,并且递归的次数不容易通过代码控制,如果超出边界就会导致栈溢出 (opens new window),在新建线程的时候没有足够的内存去创建对应的JVM Stack,那么JVM将抛出一个OutOfMemory异常(这是启动过多),这是主存储不够分配。

8、对象都是在堆上分配的吗?

不一定。满足特定条件时,它们可以在(虚拟机)栈上分配内存。

这和我们平时的理解可能有些不同。虚拟机栈一般是用来存储基本数据类型、引用和返回地址的,怎么可以存储实例数据了呢?

这是因为Java JIT(just-in-time)编译器进行的两项优化,分别称作逃逸分析 (opens new window)(escape analysis)和标量替换(scalar replacement)。

9、你怎么理解强、软、弱、虚引用?

在java.lang.ref包下就提供了三个类:SoftReference(软引用),PhantomReference(虚引用)和WeakReference(弱引用),它们分别代表了系统对对象的中的三种引用方式:软引用,虚引用以及弱引用。因此java语言对对象的引用有如下四种:

  1. 强引用:就是正常的引用。Object object = new Object(),object就是一个强引用,GC是不会清理一个强引用引用的对象的,即使面临内存溢出的情况。
  2. 软引用:SoftReference,GC会在内存不足的时候清理引用的对象。SoftReference reference = new SoftReference(object); object = null;
  3. 弱引用:GC线程会直接清理弱引用对象,不管内存是否够用。WeakReference reference = new WeakReference(object); object = null;
  4. 虚引用:和弱引用一样,会直接被GC清理,而且通过虚引用的get方法不会得到对象的引用。PhantomReference refernce = new PhantomReference(object); object = null;
 Reference re = new Reference();
        Reference reference = new Reference();//强引用 GC是不会清理一个强引用引用的对象的,即使面临内存溢出的情况。
        SoftReference softReference = new SoftReference(re);//弱引用 GC会在内存不足的时候清理引用的对象
        System.out.println(softReference.get().getClass());//引用的get方法直接获得该对象
        WeakReference weakReference = new WeakReference(re);//GC线程会直接清理弱引用对象,不管内存是否够用。
        PhantomReference phantomReference = new PhantomReference(re, new ReferenceQueue());//和弱引用一样,会直接被GC清理,而且通过虚引用的get方法不会得到对象的引用。

强引用和软引用的区别

软引用只有在内存不足的时候才会被清理,而强引用什么时候都不会被清理(程序正常运行的情况下),即使是内存不足。利用软应用这一个特性,可以做一些缓存的工作。

软引用和弱引用的区别

弱引用不会影响GC的清理,也就是说当GC检测到一个对象存在弱引用也会直接标记为可清理对象,而软引用只有在内存告罄的时候才会被清理

弱引用和虚引用的区别

说两者的区别之前要说一下ReferenceQueue的概念,ReferenceQueue是一个队列,初始化Reference的时候可以作为构造函数的参数传进去,这样在该Reference的referent域(Reference用来保存引用对象的属性)指向的引用对象发生了可达性的变化时会将该Reference加入关联的队列中,这个具体的变化根据Reference的不同而不同。

弱引用和虚引用的区别就在于被加入队列的条件不同,这里主要侧重于考虑对象所属的类重写了finalize方法,将对象的状态归纳为三种:finalizable, finalized、reclaimed,分别代表:未执行finalize函数、已经执行finalize函数,已经回收。如果没有重写finalize函数的话下面再考虑。

虚引用必须和一个ReferenceQueue联合使用,当GC准备回收一个对象的时候,如果发现该对象还有一个虚引用,就会将这个虚引用加入到与之关联的队列

弱引用是当GC第一次试图回收该引用指向的对象时会执行该对象的finalize方法,然后将该引用加入队列中,但是该引用指向的对象是可以在finlize函数中“复活”的,所以即使通过Reference的get方法得到的是null,而且reference被加入到了ReferenceQueue,这个对象仍然可以存活的,这种现象是有点违背对象正常生命周期的。

public class TestWeakReference {
        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
        public static void main(String[] args) {
            Object obj = new Object();
            WeakReference<Object> wr = new WeakReference(obj,rq);
            System.out.println(wr.get()!=null);
            obj = null;
            System.gc();
            System.out.println(wr.get()!=null);//false,这是因为WeakReference被回收
        }


    }
运行结果为:truefalse

10、常用的 JVM 参数有哪些?

11、Java 8 中的内存结构有什么变化?

Java 8 (Hotspot)移除了永久代,新增了元空间(Metaspace)。

Java 7 及以前版本的 Hotspot 方法区位于永久代,同时,永久代和堆虽然是相互隔离的,但它们使用的物理内存是连续的。而 Java 8 中的方法区存在于元空间中,同时,元空间不再与堆连续,而是存在于本地内存(Native memory)。

12、Java 8 中的永久代为什么被移除了?

1、字符串存在永久代中,容易出现性能问题和内存溢出。

2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

13、什么是类加载器?

https://blog.csdn.net/javazejian/article/details/73413292 文章来源

类加载的机制的层次结构

每个编写的".java"拓展名类文件都存储着需要执行的程序逻辑,这些".java"文件经过Java编译器编译成拓展名为".class"的文件,".class"文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的".class"文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:

加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

14、类加载器的分类及作用?

在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),下面分别介绍

##启动(Bootstrap)类加载器 启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

##扩展(Extension)类加载器 扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

##系统(System)类加载器 也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。   在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。

15、什么是双亲委派模型?

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

20170625231013755

其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

##双亲委派模式优势 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang

加载class中一个classLoader的loadclass方法解析

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //如果没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加载时进行解析
              resolveClass(c);
          }
          return c;
      }
  }

正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

自定义的类,使用默认的系统加载器

image-20230524110920550

启动类加载器,由C++实现,没有父类。

拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

自定义类加载器,父类加载器肯定为AppClassLoader。

16、为什么要打破双亲委派模型?

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的。

如果想自定义类加载器,就需要继承ClassLoader,并重写findClass,如果想不遵循双亲委派的类加载顺序,还需要重写loadClass。

Tomcat中破坏双亲委派的场景

只有官方库java.的类必须由启动类加载器加载,无法破坏,扩展类加载器和应用程序类加载器的双亲委派都是可以破坏的。

知道了理论,还需要根据实际场景,找准破坏双亲委派的位置。可以看看优秀的开源框架中是如何破坏双亲委派的,比如Tomcat:

c519374f9cd2406c9fe9ad31da0fcbdd

Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoader,WebappClassLoader继承自URLClassLoader,重写了findClass和loadClass,并且WebappClassLoader的父类加载器设置为AppClassLoader。 WebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoader,ExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。

总结回顾

  1. java 的类加载,就是获取.class文件的二进制字节码数组并加载到 JVM 的方法区,并在 JVM 的堆区建立一个用来封装 java 类相关的数据和方法的java.lang.Class对象实例。
  2. java默认有的类加载器有三个,启动类加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用程序类加载器(也叫系统类加载器)(AppClassLoader)。类加载器之间存在父子关系,这种关系不是继承关系,是组合关系。如果parent=null,则它的父级就是启动类加载器。启动类加载器无法被java程序直接引用。
  3. 双亲委派就是类加载器之间的层级关系,加载类的过程是一个递归调用的过程,首先一层一层向上委托父类加载器加载,直到到达最顶层启动类加载器,启动类加载器无法加载时,再一层一层向下委托给子类加载器加载。
  4. 加载一个类时,也会加载其父类,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器。
  5. 双亲委派的目的主要是为了保证java官方的类库<JAVA_HOME>\lib加载安全性,不会被开发者覆盖。
  6. \lib 和\lib\ext是java官方核心类库,一般不会去破坏ExtClassLoader及其以上的双亲委派。
  7. 破坏双亲委派有两种方式:第一种,自定义类加载器,必须重写findClass和loadClass;第二种是通过线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作。
  8. ClassLoader.loadClass 和 Class.forName 区别在于,ClassLoader.loadClass 不会对类进行解析和类初始化,而 Class.forName 是有正常的类加载过程的。

17、可以自定义一个 java.lang.String 吗?

假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类? 答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。

因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类, 加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理, AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader, 但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap, 父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。

三、定义自已的ClassLoader 既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。

定义自已的类加载器分为两步:

1、继承java.lang.ClassLoader

2、重写父类的findClass方法

读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findClass方法?

因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。

18、什么是 JVM 内存模型?

jvm内存模型:

image-20230525200412002

JVM的内存结构大概分为:

  • 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。

  • 方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。

  • 方法栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。

  • 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。

  • 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。

    Java内存模型

    由上述对JVM内存结构的描述中,我们知道了堆和方法区是线程共享的。而局部变量,方法定义参数和异常处理器参数就不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

    Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

    image-20230525200539141

    如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

    从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

19、JVM 内存模型和 JVM 内存结构的区别?

jvm内存模型实际是指上述的线程于主存之间的抽象关系,JVM内存结构是指jvm运行时数据分区域存储,强调度内存空间的划分。

20、什么是指令重排序?

重排序:Java 语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程叫做指令的重排序。

指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。

重排序的种类 编译期重排:编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。

运行期重排:CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化。

内存重排:程序执行一段代码,写一个普通的共享变量,其可能先被写到缓冲区然后再被写到主内存,此时指令完成的时间就被推迟了。实际表现就是内存重排。

21、内存屏障是什么?

一文解决内存屏障https://monkeysayhi.github.io/2017/12/28/%E4%B8%80%E6%96%87%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C/

22、什么是 Happens-Before 原则?

happens-before 指的是 Java 内存模型中两项操作的顺序关系。例如说操作 A 先于操作 B,也就是说操作 A 发生在操作 B 之前,操作 A 产生的影响能够被操作 B 观察到。这里的「影响」包括:内存中共享变量的值、发送了消息、调用了方法等。

3440d012034206c783a445450e75dda586232a

1 Happens-Before(先行发生)原则的定义

  • 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则(Thread Start Rule):Thread 对象 start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join()方法和 Thread.isAlive()的返回值等手段检测线程是否已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule) :一个对象的初始化完成(构造函数结束)先行发生于它的 finalize()方法的开始。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。

23、GC 是什么?为什么需要 GC?

GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。 垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

在java语言中,垃圾回收(Garbage Collection,GC)的主要作用是回收程序中不再使用的内存。 为了减轻开发人员的工作,同时增加系统的安全性和稳定性,java语言提供了垃圾回收器来自动检测对象的作用域,可自动地把不再被使用的存储空间释放掉。主要的任务是:分配内存,回收不再被引用的对象的内存空间。 垃圾回收器提高了开发人员的开发效率,保证程序的稳定性,但是也带来了问题,为了处理垃圾回收,垃圾回收器必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放后还需要处理堆中的碎片,这些操作增加JVM的负担,从而降低了程序的执行效率。 垃圾回收依据一定的算法执行的,垃圾回收算法如下:

  1. 引用计数算法:当对象被引用时,引用计数器加1,相反减1,缺点是无法解决相互引用的问题。

  2. 标记-清除算法:标记所用从根节点开始的可达对象,清除所有未被标记的对象。(适用于老年代)

  3. 复制算法:将内存空间分成两块,每次将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块。算法效率高,但是代价是将系统内存折半。(适用于新生代。存活对象少,垃圾对象多)

  4. 标记-压缩算法:该算法是对“标记-清除算法”的改进,不是直接对标记对象进行清除,而是将存活的对象压缩到内存的一端,然后直接清理掉边界以外的内存。(适用于老年代)

  5. 分代算法:根据对象的存活周期的不同将内存划分为几块,每块视为一代,一般是把 java内存堆分为新生代和老年代。根据各个年代的特点采用最适当的垃圾收集算法。

24、什么是 MinorGC 和 FullGC?

  • Minor GC

    • Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。
  • Major GC

    • Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。
  • Full GC

    • Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

25、一次完整的 GC 流程是怎样的?

一、可达性分析算法(GC Roots) 有一种引用计数法,可以用来判断对象被引用的次数,如果引用次数为0,则代表可以被回收。

这种实现方式比较简单,但对于循环引用的情况束手无策,所以 Java 采用了可达性分析算法。

即判断某个对象是否与 GC Roots 的这类对象之间的路径可达,若不可达,则有可能成为回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

在 Java 中,可作为 GC Roots 的对象包括以下几种:

虚拟机栈(本地变量表)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中引用的对象

2.1 为何新生代要分为三个区 这里需要介绍新生代的垃圾回收算法——复制算法。该算法的核心是将可用内存按容量划分为大小相等的两块,每次回收周期只用其中一块,当这一块的内存用完,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间清理掉。

优点:不必考虑内存碎片问题;效率高。 缺点:可用容量减少为原来的一半,比较浪费。 2.2 新生代对象的分配和回收 (1)基本上新的对象优先在 Eden 区分配;

(2)当 Eden 区没有足够空间时,会发起一次 Minor GC;

(3)Minor GC 回收新生代采用复制回收算法的改进版本,即

from 区和 to 区的两个交换区,这两个区只有一个区有数据 采用8:1:1的默认分配比例(-XX:SurvivorRatio默认为8,代表 Eden 区与 Survivor 区的大小比例)

2.3 老年代对象的分配和回收 (1)老年代的对象一般来自于新生代中的长期存活对象。这里有一概念叫做年龄阈值,每个对象定义了年龄计数器,经过一次 Minor GC (在交换区)后年龄加1,对象年龄达到15次后将会晋升到老年代,老年代空间不够时进行 Full GC。当然这个参数仍是可以通过 JVM 参数(-XX:MaxTenuringThreshold,默认15)来调整。

(2)大对象直接进入老年代。即超过 Eden 区空间,或超过一个参数值(-XX:PretenureSizeThreshold=30m,无默认值)。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

(3)对象提前晋升到老年代(组团)。动态年龄判定:如果在 Survivor 区中相同年龄所有对象大小总和大于 Survivor 区大小的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无须等到自己的晋升年龄。

三、JVM完整的GC流程 对象的正常流程:Eden 区 -> Survivor 区 -> 老年代。

新生代GC:Minor GC;老年代GC:Full GC,比 Minor GC 慢10倍。

【总结】:内存区域不够用了,就会引发GC,JVM 会“stop the world”,严重影响性能。Minor GC 避免不了,Full GC 尽量避免。

【处理方式】:保存堆栈快照日志、分析内存泄漏、调整内存设置控制垃圾回收频率,选择合适的垃圾回收器等。

26、JVM 如何判断一个对象可被回收?

一、引用计数算法:   判断对象的引用数量:     通过判断对象的引用数量来决定对象是否可以被回收;

每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1;

任何引用计数为0的对象实例可以被当作垃圾收集;

优缺点:     优点:执行效率高,程序执行受影响较小;

缺点:无法检测出循环引用的情况,导致内存泄漏;

二、可达性分析算法: 通过判断对象的引用链是否可达来决定对象是否可以被回收;

jvm要做垃圾回收时,首先要判断一个对象是否还有可能被使用。那么如何判断一个对象是否还有可能被用到?

如果我们的程序无法再引用到该对象,那么这个对象就肯定可以被回收,这个状态称为不可达。当对象不可达,该对象就可以作为回收对象被垃圾回收器回收。

那么这个可达还是不可达如何判断呢?

答案就是GC roots ,也就是根对象,如果从一个对象没有到达根对象的路径,或者说从根对象开始无法引用到该对象,该对象就是不可达的。

以下三类对象在jvm中作为GC roots,来判断一个对象是否可以被回收 (通常来说我们只要知道虚拟机栈和静态引用就够了)

虚拟机栈(JVM stack)中引用的对象(准确的说是虚拟机栈中的栈帧(frames)) 我们知道,每个方法执行的时候,jvm都会创建一个相应的栈帧(栈帧中包括操作数栈、局部变量表、运行时常量池的引用),栈帧中包含这在方法内部使用的所有对象的引用(当然还有其他的基本类型数据),当方法执行完后,该栈帧会从虚拟机栈中弹出,这样一来,临时创建的对象的引用也就不存在了,或者说没有任何gc roots指向这些临时对象,这些对象在下一次GC时便会被回收掉

方法区中类静态属性引用的对象 静态属性是该类型(class)的属性,不单独属于任何实例,因此该属性自然会作为gc roots。只要这个class存在,该引用指向的对象也会一直存在。class 也是会被回收的,在面后说明

本地方法栈(Native Stack)引用的对象

一个class要被回收准确的说应该是卸载,必须同时满足以下三个条件

堆中不存在该类的任何实例 加载该类的classloader已经被回收

该类的java.lang.Class对象没有在任何地方被引用,也就是说无法通过反射再带访问该类的信息

27、常用的垃圾收集器有哪些?

垃圾回收主要是在JVM内存模型的堆中,堆内存的区域划分:

43b976d74c200c390a76899ff2ca3282ecac0b

  • 因为虚拟机使用的垃圾回收算法是分代收集算法,所以堆内存被分为了新生代和老年代
  • 新生代使用的垃圾回收算法是复制算法,所以新生代又被分为了 Eden 和Survivor;空间大小比例默认为8:2
  • Survivor又被分为了S0、S1,这两个的空间大小比例为1:1

内存分配以及垃圾回收

  1. 对象优先在Eden区进行分配,如果Eden区满了之后会触发一次Minor GC
  2. Minor GC之后从Eden存活下来的对象将会被移动到S0区域,当S0内存满了之后又会被触发一次Minor GC,S0区存活下来的对象会被移动到S1区,S0区空闲;S1满了之后在Minor GC,存活下来的再次移动到S0区,S1区空闲,这样反反复复GC,每GC一次,对象的年龄就涨一岁,默认达到15岁之后就会进入老年代,对于晋身到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold设置
  3. 在Minor GC之后需要的发送晋身到老年代的对象没有空间安置,那么就会触发Full GC (这步非绝对,视垃圾回收器决定)

垃圾回收器概览

03b0e4169f37b5b3c7e831a7494572b5994530

从上图可以看出:

  • 新生代可以使用的垃圾回收器:Serial、ParNew、Parallel Scavenge
  • 老年代可以适用的垃圾回收器:CMS、Serial Old、Parallel Old
  • G1回收器适用于新生代和老年代
  • 相互之间有连线的表示可以配合使用

Serial收集器

这是个单线程收集器,发展历史最悠久的收集器,当它在进行垃圾收集工作的时候,其他线程都必须暂停直到垃圾收集结束(Stop The World)。

虽然Serial收集器存在Stop The World的问题,但是在并行能力较弱的单CPU环境下往往表现优于其他收集器;因为它简单而高效,没有多余的线程交互开销;Serial对于运行在Client模式下的虚拟机来说是个很好的选择

使用-XX:+UseSerialGC参数可以设置新生代使用这个Serial收集器

ParNew收集器

ParNew收集器是Serial收集器的多线程版本;除了使用了多线程进行垃圾收集以外,其他的都和Serial一致;它默认开始的线程数与CPU的核数相同,可以通过参数-XX:ParallelGCThreads来设置线程数。

从上面的图可以看出,能够与CMS配合使用的收集器,除了Serial以外,就只剩下ParNew,所以ParNew通常是运行在Server模式下的首选新生代垃圾收集器

使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器

Parallel Scavenge收集器

Parallel Scavenge收集器依然是个采用复制算法的多线程新生代收集器,它与其他的收集器的不同之处在于它主要关心的是吞吐量,而其他的收集器关注的是尽可能的减少用户线程的等待时间(缩短Stop The World的时间)。吞吐量=用户线程执行时间/(用户线程执行时间+垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集花费时间1分钟,那么吞吐量就是 99%

停顿时间越短适合需要和用户进行交互的程序,良好的响应能够提升用户的体验。而高效的吞吐量可以充分的利用CPU时间,尽快的完成计算任务,所以Parallel Scavenge收集器适用于后台计算型任务程序。

-XX:MaxGCPauseMillis可以控制垃圾收集的最大暂停时间,需要注意不要以为把这个时间设置的很小就可以减少垃圾收集暂用的时间,这可能会导致发生频繁的GC,反而降低了吞吐量

-XX:GCTimeRatio设置吞吐量大小,参数是取值范围0-100的整数,也就是垃圾收集占用的时间,默认是99,那么垃圾收集占用的最大时间 1%

-XX:+UseAdaptiveSizePolicy 如果打开这个参数,就不需要用户手动的控制新生代大小,晋升老年代年龄等参数,JVM会开启GC自适应调节策略

Serial Old收集器

Serial Old收集器也是个单线程收集器,适用于老年代,使用的是标记-整理算法,可以配合Serial收集器在Client模式下使用。

它可以作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。(后面CMS详细说明)

Parallel Old收集器

Parallel Old收集器可以配合Parallel Scavenge收集器一起使用达到“吞吐量优先”,它主要是针对老年代的收集器,使用的是标记-整理算法。在注重吞吐量的任务中可以优先考虑使用这个组合

-XX:+UseParallelOldGc设置老年代使用该回收器。

XX:+ParallelGCThreads设置垃圾收集时的线程数量。

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,在互联网网站、B/S架构的中常用的收集器就是CMS,因为系统停顿的时间最短,给用户带来较好的体验。

-XX:+UseConcMarkSweepGC设置老年代使用该回收器。

-XX:ConcGCThreads设置并发线程数量。

CMS采用的是标记-清除算法,主要分为了4个步骤:

  • 初始化标记

  • 并发标记

  • 重新标记

  • 并发清除 初始化标记和重新标记这两个步骤依然会发生Stop The World,初始化标记只是标记GC Root能够直接关联到的对象,速度较快,并发标记能够和用户线程并发执行;重新标记是为了修正在并发标记的过程中用户线程产生的垃圾,这个时间比初始化标记稍长,比并发标记短很多。整个过程请看下图

    e646b3886e6c3b8bbae4770af2727f9738b883

    优点

    CMS是一款优秀的收集器,它的主要优点:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

    缺点

    • CMS收集器对CPU资源非常敏感。 在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

    • 无法处理浮动垃圾。 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,回收阀值可以通过参数-XX:CMSInitiatingoccupancyFraction来设置;如果回收阀值设置的太大,在CMS运行期间如果分配大的对象找不到足够的空间就会出现“Concurrent Mode Failure”失败,这时候会临时启动SerialOld GC来重新进行老年代的收集,这样的话停顿的时间就会加长。

    • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大问题,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。为了解决这个问题CMS提供了一个参数-XX:+UseCMSCompactAtFullCollecion,如果启用,在Full GC的时候开启内存碎片整理合并过程,由于内存碎片整理的过程无法并行执行,所以停顿的时间会加长。考虑到每次FullGC都要进行内存碎片合并不是很合适,所以CMS又提供了另一个参数-XX:CMSFullGCsBeforeCompaction来控制执行多少次不带碎片整理的FullGC之后,来一次带碎片整理GC

G1收集器 G1是一款面向服务端应用的垃圾回收器。

并行与并发:与CMS类似,充分里用多核CPU的优势,G1仍然可以不暂停用户线程执行垃圾收集工作 分代收集:分代的概念依然在G1保留,当时它不需要和其他垃圾收集器配合使用,可以独立管理整个堆内存 空间的整合:G1整体上采用的是标记-整理算法,从局部(Region)采用的是复制算法,这两种算法都意味着G1不需要进行内存碎片整理 可预测的停顿:能够让用户指定在时间片段内,消耗在垃圾收集的时间不超过多长时间。 Region 虽然在G1中依然保留了新生代和老年代的概念,但是采用的是一种完全不同的方式来组织堆内存,它把整个堆内存分割成了很多大小相同的区域(Region),并且新生代和老年代在物理上也不是连续的内存区域,请看下图:

88f2e6180922e49e2c70789b304e786fca9c98

每个Region被标记了E、S、O和H,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象,当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。Region区域的内存大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是2的幂次方,如:1M、2M、4M、8M

G1的GC模式

  • 新生代GC:与其他新生代收集器类似,对象优先在eden region分配,如果eden region内存不足就会触发新生代的GC,把存活的对象安置在survivor region,或者晋升到old region
  • 混合GC:当越来越多的对象晋升到了old region,当老年代的内存使用率达到某个阈值就会触发混合GC,可以通过参数-XX:InitiatingHeapOccupancyPercent设置阈值百分比,此参数与CMS中-XX:CMSInitiatingoccupancyFraction的功能类似;混合GC会回收新生代和部分老年代内存,注意是部分老年代而不是全部老年代;G1会跟踪每个Region中的垃圾回收价值,在用户指定的垃圾收集时间内优先回收价值最大的region
  • Full GC:如果对象内存分配速度过快,混合GC还未回收完成,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,此过程与CMS类似,会导致异常长时间的暂停时间,尽可能的避免full gc.

28、常用的垃圾回收算法有哪些?

判断一个对象是否为死亡状态的常用算法有两个:引用计数器算法和可达性分析算法。

引用计数算法(Reference Counting) 属于垃圾收集器最早的实现算法了,它是指在创建对象时关联一个与之相对应的计数器,当此对象被使用时加 1,相反销毁时 -1。当此计数器为 0 时,则表示此对象未使用,可以被垃圾收集器回收。 可达性分析算法(Reachability Analysis) 是目前商业系统中所采用的判断对象死亡的常用算法,它是指从对象的起点(GC Roots)开始向下搜索,如果对象到 GC Roots 没有任何引用链相连时,也就是说此对象到 GC Roots 不可达时,则表示此对象可以被垃圾回收器所回收

当确定了对象的状态之后(存活还是死亡)接下来就是进行垃圾回收了,垃圾回收的常见算法有以下几个:

  • 标记-清除算法;
  • 标记-复制算法;
  • 标记-整理算法。

知识扩展 CG Roots 在 Java 中可以作为 CG Roots 的对象,主要包含以下几个:

所有被同步锁持有的对象,比如被 synchronize 持有的对象; 字符串常量池里的引用(String Table); 类型为引用类型的静态变量; 虚拟机栈中引用对象; 本地方法栈中的引用对象。

29、什么是内存泄漏?

使用Java编写程序时,我们使用new关键字创建对象。而且我们还不需要专门在对象使用完成后去释放其占用的内存,这是因为Java有专门的垃圾回收器来负责删除不需要的对象。只要不被使用的对象有垃圾回收器回收,那么程序会处于正常运行的状态,但是垃圾回收器无法删除那些不被使用的对象时,我们的Java程序则可能发生了内存泄漏。

内存泄漏指的是JVM中某些不再需要使用的对象,仍然存活于JVM中而不能及时释放而导致内存空间的浪费。Java中内存泄漏的原因有多种,这些众多的因素会导致Java程序产生不同类型的内存泄漏,随着时间的推移,内存泄漏会使程序增加额外的内存资源占用,从而导致程序性能下降。

垃圾回收器会回收长时间没有引用的对象,但是它不会回收那些还存在引用的对象,这就是产生内存泄漏的原因。

内存溢出 out of memory :

指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。

内存泄露与内存溢出二者的关系:

  • 内存泄漏的堆积最终会导致内存溢出
  • 内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
  • 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

堆和栈的内存泄漏

Java中,我们可能会遇到栈内存泄露和堆内存泄漏。

其中堆内存泄漏是由于创建后的对象一直存在于堆中,不再需要的对象其引用一直没有被移除。这些无用的对象会慢慢占用内存,最后导致内存溢出。

栈内存泄漏由于方法不断被调用,但是一直没有退出方法。这种情况可能发生在无限循环或递归掉用时,最终导致栈内存溢出。

30、为什么会发生内存泄漏?

内存泄漏分类

  • 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  • 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  • 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  • 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

Java中内存泄漏主要是因为不能正确释放不需要的资源,长生命周期对象持有短生命周期对象的引用。

  • 静态字段

静态字段引起的内存泄漏比较常见,如果某个不需要的类中含有静态字段,那么就会造成内存泄漏。单例模式中如果持有其他的类引用就会造成内存泄漏,静态集合如HashMap,LinkedList等持有的一些对象没有及时释放等。

  • Thread Local

threadlocal引用一个对象使用完成后并没有被及时remove掉,线程一直存活的情况下(使用线程池时)就会发生内存泄漏。

大多时候内存泄漏都是由于开发人员的代码错误导致的,要防止这种内存泄漏,就需要编写必要的代码来配合垃圾回收器释放资源。

31、如何防止内存泄漏?

  • 使用最新稳定版本的Java
  • 尽量减少使用静态变量,使用完之后及时赋值 null,移除引用
  • 明确对象的有效作用域,尽量缩小对象的作用域。局部变量回收会很快。
  • 减少长生命周期对象持有短生命周期的引用
  • 各种连接应该及时关闭(数据库连接,网络,IO等)
  • 使用内存泄漏检测工具如MAT,Visual VM,jprofile 等
  • 避免在代码中使用System.gc()
  • 避免使用内部类

32、什么是直接内存?

  • 直接内存:概指系统内存,而非堆内存 (opens new window),不指定大小时它的大小默认与堆的最大值-Xmx参数值一致。使用直接内存则是直接使用操作系统内存,而不是使用的是堆内存,减少了内核态与用户态的反复切换,效率更高。
  • 非直接内存: 也可以称之为堆内存,运行JVM (opens new window)都会预先分配一定内存,我们把JVM管理的这些内存称为堆内存(非操作系统直接内存),JVM会对这些内存空间的分配和回收进行管理。

33、直接内存有什么用?

所谓 直接的关系指的是与底层操作系统的关系。

直接 非直接内存的概念与NIO有非常大的关联;

在NIO之前,java.io 的方式是:

 磁盘IO --> 直接内存[系统内核态] -->   非直接(堆)内存[用户态]  -->  直接内存[系统内核态] --> 磁盘IO

而NIO中,对文件的读写不再跟堆内存关联

 磁盘IO --> 系统直接内存 --> 磁盘IO

读写文件时可以直接申请堆外内存。

优点

  • 避免内核态和用户态之间反复切换,实现文件的高效存取;
  • 非JVM管理内存,能减少GC时造成的STW(stop the world)操作。

34、怎样访问直接内存?

我们知道直接内存直接使用操作系统内存,避免了反复的拷贝。

直接内存的使用通过:allocateDirect创建,需要注意的时,直接内存的申请成本比申请普通堆内存更大;

基于以上特性,直接内存在文件较大时会有不错的表现,由于申请开销问题,当操作海量的小文件时我们就需要慎重考虑是否使用直接内存了,此时还会带来内存碎片化的问题。

35、常用的 JVM 调优命令有哪些?

36、常用的 JVM 问题定位工具有哪些?

37、常用的主流 JVM 虚拟机都有哪些?

1、Oracle的HotSpot虚拟机;

2、BEA System的JRockit虚拟机;

3、IBM公司的J9虚拟机。并称“三大商业Java虚拟机”。

HotSpot VM,是 Sun JDK 和 OpenJDK 中所带的虚拟机,也是目前使用范围最广的Java虚拟机。

虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。