前言:
计算机是二进制的系统,他只认识 01010101,但像我们编写的HelloWord.java,计算机是不认识的,因此就需要编译,由javac编译成字节码文件.class,因为JVM只认识.class文件,再由JVM编译成计算机认识的文件,对于电脑系统来说,文件代表一切,这也是说Java是跨平台语言的原因。
再看看JDK、JRE、JVM的关系,JDK中包括了JRE,JRE中包括了JVM。以下是Java官方图片。
下面是Java程序运行机制详细说明
Java程序运行机制步骤
- 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
- 再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
- 运行字节码的工作是由解释器(java命令)来完成的。
一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
一.Java内存区域
JVM 的主要组成部分及其作用
JVM包含两个子系统和两个组件,
两个子系统:Class loader(类加载器)、Execution engine(执行引擎);
两个组件:Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类加载器):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM内存。
作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
JVM 运行时数据区
Java虚拟机主要分为以下五个区:
- 方法区
- 堆内存
- Java虚拟机栈
- 本地方法栈
- 程序计数器
一、方法区(METHOD AREA)
- 方法区是所有线程共享的内存区域,用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。个人理解的方法区就是封装了一个java class的所有信息。
- 方法区有个别命,叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
- 方法区里有一个运行时常量池,存放静态编译产生的字面量和符号引用,运行时生成的常量也会存在这个常量池中。
二、Java堆 (HEAP)
- Java堆也是所有线程所共享的一块内存,在虚拟机启动时创建,此内存区域的唯一目的就是存放几乎所有的对象实例,因此该区域经常发生垃圾回收操作,因此也被成为“GC堆”。
- 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
- 如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
三、虚拟机栈(JAVA STACK)
- java虚拟机是线程私有的,它的生命周期和线程相同。
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。【解释:每个虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。】
解析栈帧:
- 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。returnAddress中保存的是return后要执行的字节码的指令地址。
- 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
- 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
- 出口:出口正常的话就是return,不正常的话就是抛出异常。
四、本地方法栈 (NATIVE METHOD STACK)
- 本地方法栈与虚拟机栈的作用是一样,虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。
- Native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。
五、程序计数器 (PEOGRAM COUNTER REGISTER)
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。为了保证程序(进程)能够连续地执行下去,CPU必须具有某些手段确定下一条指令的地址。而程序计数器正是起到这种作用,所以通常又称为指令计数器。
- 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。
- 如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。
- 该内存区域是唯一的java虚拟机规范没有规定任何OOM(OutOfMemoryError)情况的区域。
深拷贝和浅拷贝
- 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址。
- 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。
- 使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
- 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
- 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
堆栈的区别
- 栈:先进后出的原则,物理地址分配是连续的,所以性能快。堆:堆的物理地址分配是不连续的,所以性能慢些。【因此在GC的时候也要考虑到堆的不连续分配,所以有各种算法,比如,标记清除,复制,标记整理(压缩),引用计数法,分代(即新生代使用复制算法,老年代使用标记—压缩)】。
- 栈:分配的内存大小要在
编译期
就确认,大小是固定的。堆:分配的内存是在运行期
确认的,大小不固定,但远大于栈。 - 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。堆存放:对象的实例和数组。因此该区更关注的是数据的存储。
- 栈:只对于线程是可见的。所以也是线程私有,他的生命周期和线程相同。堆:对于整个应用程序都是共享、可见的。
队列和栈是什么?有什么区别?
- 队列和栈是两种操作受限的线性表,都是线性结构,都是被用来预存储数据的。
- 队列是先进先出(FIFO),即先进先出原则,只允许在表尾插入数据元素,在表头删除数据元素。
- 栈为先进后出(FILO),即先进后出原则,栈的插入和删除操作只允许在表的尾端进行。
二.HotSpot虚拟机对象探秘
HotSpot是较新的Java虚拟机,用来代替JIT(Just in Time),Java原先是把源代码编译为字节码在虚拟机执行,但这样执行速度较慢,而HotSpot将常用的部分代码编译为本地(原生,native)代码,提高执行性能。
HotSpot包括一个解释器和两个编译器(client 和 server,二选一),解释与编译混合执行模式,默认启动解释执行。
- 编译器:Java源代码被编译器编译成class文件(字节码),java字节码在运行时可以被动态编译(JIT)成本地代码(前提是解释与编译混合执行模式且虚拟机不是刚启动时)。
- 解释器: 解释器用来解释class文件(字节码),Java是解释语言。
对象的创建
- new 指令 class。
- 检查class是否被加载。
- java堆分配内存。
- 对象内存空间初始化,对象头初始化。
- java的init方法。
为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
- 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
- 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
- 对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。
对象的访问定位
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:
- 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储了对象的句柄地址,而句柄中包含了对象的实例数据与类型数据各自的具体地址信息。
- 直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中直接存储的就是对象的地址。
总结:
- 使用句柄访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时(垃圾收集时移动对象是非常普遍的行为)只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
- 使用直接指针访问最大的好处就是速度快,它节省了一次指针定位的时间开销。由于对象的访问在
Java
中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
三.内存泄露/溢出异常
内存泄漏(memory leak)
内存泄漏:指无用的对象或者变量一直被占据在内存中。
理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。但是即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
内存溢出(out of memory)
内存溢出:指程序申请内存时,内存不够用,比如给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,此时就会报错OOM,即所谓的内存溢出。
通俗的说,就是停车场(Java堆)保安(GC)让很久不用的废弃车子(无用的对象)从车位上挪走,但是这个车子又没办法挪走。这就是内存泄漏。停车场所有的车位都有车子占用了,再来车子没地了,或者说给你一个小汽车的停车位(int),你非要停一辆高铁(Long),这就是内存溢出。
内存泄露量大到一定程度会导致内存溢出。但是内存溢出不一定是内存泄露引起的。
四.垃圾收集器
简述Java垃圾回收机制
JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
GC是什么?为什么要GC?
垃圾回收机制简称GC(Gabage Collection)。GC主要用于Java堆的管理。Java 堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
因为程序在运行过程中,会产生大量的内存垃圾,GC是不定时自动到堆内存中清理不可达对象。程序员唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器。
垃圾回收的优点和原理
优点:
- 程序员不再考虑内存管理问题;
- 有效的防止了内存泄露,可以高效的使用空闲内存。
- 由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。
原理:
- 作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收,但是不能实时的对某一对象或者所有对象进行垃圾回收。我们可以调用System.gc()让GC运行,但无法保证GC一定会执行。
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
可以。
程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
Java 中都有哪些引用类型?
强引用:发生 gc 的时候不会被回收。
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
弱引用:有用但不是必须的对象,在下一次GC时会被回收。
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
怎么判断对象是否可以被回收?
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:
- 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
- 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
在Java中,对象什么时候可以被垃圾回收?
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
JVM中的永久代中会发生垃圾回收吗?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区
(注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)
说一下 JVM 有哪些垃圾回收算法?
1).Mark-Sweep(标记-清除)算法
这是最基础的算法,该算法就是标记出需要被回收的对象,等到需要执行GC操作时将标记的对象一并清除,实现垃圾回收。改方法简单,效率高,但是有个缺点就是会导致内存碎片。
2).Copying(复制)算法
Copying算法是将内存区域划分为两块相同大小的子区域,并且在其中的一块中执行对象分配,等到这一块的内存用完了,就将该区域还存活着的对象复制到另外一块内存区域上面,然后再把已使用的内存空间一次清理掉,这样就不会导致内存碎片的问题,但是有个明显的缺点,每次只能使用到一半的内存,对内存压力大。
3).Mark-Compact(标记-整理)算法
Mark-Compact算法是在Mark-Sweep算法的基础上进行了改进,算法标记跟Mark-Sweep一样,只是在标记完之后,将标记的对象向一端移动,然后清理掉边界以外的内存区域,这样就解决了内存碎片化的问题
4).Generational Collection(分代收集)算法
这是目前Jvm使用的垃圾回收算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。分为老年代(Tenured Generation)和新生代(Young Generation)。老年代的内存区域的对象一般回收频率比较低,采用了Mark-Compact算法,而新生代的内存区域由于每次需要回收大量对象,回收频率较高,所以将该区域又划分成了一个较大的Eden空间和两个较小的Suivivor空间,每次使用Eden空间和一个Survivor空间的,当需要回收垃圾时,将Eden空间和该Survivor空间的存活对象复制到另外一块Survivor空间上,然后清理到Eden和刚才使用的Survivor空间,实现垃圾回收机制。
说一下 JVM 有哪些垃圾回收器?
新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
新生代收集器:
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
老年代收集器:
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
G1(Garbage First)收集器 (标记-整理算法):
- Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候会产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记整理的算法进行垃圾回收。
简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
五.内存分配与回收策略以及Minor GC和Major GC
Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点新生代分为:Eden 空间、From Survivor、To Survivor 空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
JVM堆内存图
永久代
- 指内存的永久储存区域
- 存储程序运行时长期存活的对象,比如类、方法、常量、属性等元数据
- GC不会在程序运行期间对永久代进行清理
- 永久代的内存会随着元数据加载的增加而增加,当超出内存时会遇到OOM错误(这种情况很少,因此不是内存设置的主要区域)。
新生代
- 默认约占1/3的堆内存
- JVM创建新对象的地方(大对象除外)
- 对象的创建和销毁最频繁的区域
- 垃圾回收的主要区域
- 特点:朝生夕死
-
设置Survivor区的意义:减少进入老年代的对象。如果不设置幸存者区,那么每次MinorGC存活的对象都会进入老年代,当老年代内存满了之后会进行MajorGC,非常影响程序的性能。
-
设置两个Survivor区的目的:防止内存碎片化。因为MinorGC将Eden和SurvivorFrom中存活下来的对象复制到SurvivorTo区,From区和To区交换,所以每次存活的对象都会进入一个空的To区,然后依次分配,保证了分配的内存是连续的,也就不会出现内存碎片化。并且SurvivorFrom和SurvivorTo区的内存大小必须严格相等
老年代
- 存放有长生命周期的对象(指新生代中经过多次MinorGC达到老年代标准的对象)和大对象
- 垃圾回收为MajorGC
- 对象比较稳定,不会频繁的触发垃圾回收
- 老年代内存空间不足时,会抛出OOM
MinorGC 的过程(复制->清空->互换)
MinorGC是指发生在新生代的GC,它决定了新生代对象的生命周期,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;MinorGC 的原理就是将Eden和SurvivorFrom中存活下来的对象复制到SurvivorTo区,From区和To区交换。
MajorGC/Full GC
标记清除算法。Major GC/Full GC 是指发生在老年代的 GC,出现了Major GC通常会伴随至少一次Minor GC。Major GC的速度通常会比 Minor GC慢10倍以上。
六.虚拟机类加载机制
简述java类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
描述一下JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
- 1.隐式装载:程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中;
- 2.显式装载:通过class.forname()等方法,显式加载需要的类。
类加载器是什么,类加载器有哪些?
类加载器分类:
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它是用来加载Java的扩展库。JVM会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。
- 系统类加载器(system class loader):也叫应用类加载器,它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader0来获取它。
- 用户自定义类加载器:通过继承 java.lang.ClassLoaderClassLoaderClassLoaderClassLoader类的方式实现。
类装载的执行过程
类装载分为以下 5 个步骤:
- 加载:根据查找路径找到相应的 class 文件然后导入;
- 验证:检查加载的 class 文件的正确性;
- 准备:给类中的静态变量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作。
双亲委派模型介绍
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载器无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
总结一下:当一个类加载器接收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
七.JVM调优
说一下 JVM 调优的工具
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
- jconsole:用于对 JVM 中的内存、线程和类等进行监控;
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
常用的 JVM 调优的参数都有哪些?
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。