JVM
1、JVM加载class文件的原理
JVM中类的装载是由Classloader和它的子类来实现的,Java ClassLoader是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。
Java中所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,几乎不关系类的加载,因为这些都是隐式装载的,除非有特殊的用法,像反射,就需要显式的加载所需要的类。
类装载方式,有两种
1.隐式装载,程序在运行过程中当碰到通过new等方法创建对象时,就会隐式调用类加载器加载对应类到JVM中
2.显式装载,通过class.forname()等方法,显式加载需要的类。
Java类的加载是动态的,它并不会一次性将所有的类全部加载后在运行,而是保证程序运行的基础类完全加载到JVM中,其他类需要的时候在加载。
2、什么是Java虚拟机?
Java虚拟机是一个可执行Java字节码的虚拟机进程。java源文件被编译成能被Java虚拟机执行的字节码文件。
3、JVM最大内存限制
1.堆内存分配
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM分配的最大内存由-Xmx指定,默认物理内存的1/4;默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆的内存大于70%的时候,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xmx,-Xms相等避免在每次GC后调整堆的大小。
2.非堆内存分配
JVM使用-XX:PermSize设置非堆内存的初始值,默认时物理内存的1/64;
由XX:MaxPermsize设置最大非堆内存的大小,默认是物理内存的1/4。
3.JVM的最大内存
首先JVM的内存限制于实际的最大物理内存,假设物理内存无限大,JVM内存的最大值跟操作系统有关。比如说32位处理器虽然可控内存空间由4
GB,但是具体的操作系统会给出一个限制,一般是2-3GB,而64位就不会有限制。
4、JVM是如何实现线程的
线程是比进程更轻量级的调度执行单位。线程可以把一个进程的资源分配和执行调度分开。一个进程里面可以启动多条线程,各个线程可共享该进程的资源(内存地址,文件IO等),又可以独立调度。线程是CPU调度的基本单位。
主流的OS都提供线程实现。Java语言提供堆线程操作的同一API,每个已经执行start(),且还未结束的java.lang.Thread类的实例,代表了一个线程。
Thread类的关键方法,都声明位Native。这意味着这个方法无法或没有使用平台无关的手段来实现。
JVM实现线程的方式:
1.使用内核线程实现
内核线程就是直接由操作系统内核支持的线程。内核来完成线程切换;内核通过调度器调度线程,并将线程的任务映射到各个CPU上,每个内核线程可以视为内核的一个分身。程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口–轻量级进程,轻量级进程通常意义上的线程,由于每个轻量级进程都由一个内核线程支持的,因此只有先支持内核级线程,才能有轻量级进程,这种轻量级和内核线程之间1:1的关系称为一对一的线程模型。内核线程保证了每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响进程继续工作
2.使用用户线程实现
用户线程是指完全建立在用户空间的线程库上。这种不需要切换内核态,效率高消耗低,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程于用户线程之间1:N的关系称为1对多的线程模型。
3.用户线程加轻量级进程混合实现
可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低整个进程被完全阻塞的风险
5、JVM的内存模型
Java内存模型(JMM),JMM决定一个线程对共享变量写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在。它覆盖了缓存,写缓冲区,寄存器以及其他硬件和编译器优化。
6、在Java虚拟机中,哪些对象可以看作为root
1.虚拟机栈中的引用对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用对象
4.本地方法栈中JNI引用对象
7、GC中如何判断对象是否需要被回收
即使在可达性分析算法中不可达的对象,也并非是”非回收不可“的,这时候他们暂时处于”等待“阶段,要真正宣告一个对象回收,至少要经历两次标记过程:如果对象哎进行可达性分析后发现没有和GC roots相连接的引用,那么它将被第一次标记,并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize方法已经被虚拟机调用过了,虚拟机将这两种情况都视为”没有必要执行“(即意味着直接回收)。
如果这个对象被判定为有必要执行finalize()方法,那么会将这个对象放置在一个叫F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,但虚拟机只会触发这个方法,并不会等待它运行结束。因为如果一个对象在Finlize()方法中执行太慢或者死循环,导致F-Queue队列中其他对象永远处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱回收的最后一次机会,GC将对F-Queue队列中的对象进行第二次小规模标记,如果对象在finalize()方法中跳出回收——只要重新与引用链上的任何一个对象建立关联。比如把自己(this)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移除”即将回收“的集合;如果对象这是时候还没有逃脱,那就真的GG了
8、Java虚拟机的作用
解释运行字节码程序消除平台相关性
JVM将java字节码解释为具体平台的具体指令。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的代码。而引入JVM后,Java语言在不同的平台运行不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改的运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令
9、请说明一下Eden区和survial区的含义以及工作原理
目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分为Eden空间、From Survivor和To Survivor三块区域。
我们把Eden:From survivor:To survivor空间大小设成8:1:1,对象总是在Eden区出生,From Survivor保存当前幸存对象,Tosurvivor为空。一次gc发生后:
1.Eden区或者的对象 + Fromsurvivor存储的对象被复制到To suivivor;
2.清空Eden和From Survivor;
3.颠倒From Survivor和From Survivor的逻辑关系:From变To,To变From。
可以看出,只有在Eden空间快满的时候才会触发minor GC。而Eden空间占新生代的绝大部分,所以minor GC的频率得以降低。当然,使用两个Survivor这种方式也有代价10%的空间浪费,复制对象的开销。
10、要求stop the world时间非常短,如何设计垃圾回收机制
绝大多数新创建的对象分配在Eden区。在Eden区发生一次GC后,存活的对象移动到其中一个Survivor区。一旦这个Survior区满了,就将存活的对象移动到另一个Survivor区。之前那个清空,没有数据。经过多次这样步骤后依然存活的对象移动到老年代。
11、JVM分区
五个区域,多线程共享内存区域有:方法区、堆。每一个线程独享内存:Java栈、本地方法栈、程序计数器。
1.程序计数器:较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。
2.Java栈:线程私有,生命周期和线程,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等性息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用。
3.本地方法栈:本地方法栈保存的时native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机中创建栈帧,JVM只是简单的动态链接并直接调用native方法;
4.堆:Java堆是我们需要重点关注的一块区域,因为涉及到内存的分配(new关键字,反射等)与回收(回收算法,收集器等);
5.方法区:也叫永久区,用于存储已经虚拟机加载的类信息,常量,静态变量等数据。但在jdk1.8中将方法区移动到直接内存。
6.运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面和符号引用。
7.直接内存:不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域;
7.1如果使用了NIO,这块区域会被频繁使用,在Java堆内可以用directbytebuffer对象直接引用并操作;
7.2这块内存不受Java堆大小限制,但受本机总内存限制,可以通过MaxdirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;
12、类加载过程
JVM类加载机制分为五个部分:加载、验证、准备、解析、初始化。
加载->验证->准备->解析->初始化->使用->卸载;
其中验证、准备、解析过程统称连接。
1.加载:加载时类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据入口。这里不一定非要从Class文件中获取,既可以从jar包和war包中读取,也可以在运行时计算生成(动态代理),也可以由其他文件生成(比如将JSP文件转换成对应的class类)。
2.验证:这一阶段的主要目的是为了确保Class文件的字节流包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3.准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间,初始值概念问题:
Public static int v = 8080;
实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值8080的 putstatic指令是被程序编译后,存放于类构造器<client>方法中。
但如果加了final修饰,在编译阶段会为ConstantValue属性,在准备阶段虚拟会根据ConstantValue属性将v赋值为8080。
4.解析
是指虚拟机将常量池中的符号引用替换为直接引用的过程,符号引用就是class文件中的:CONSTANT_Class_info,CONSTANT_Field_info,CONSTANT_Method_info等类型的常量。
符号引用和直接引用的概念:
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
5.初始化
是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以定义自定义类加载器以外,其他操作都有JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<client>方法方法执行前,父类的<client>方法已经执行完毕。如果一个类中没有堆静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>方法。
以下情况不能执行类初始化:
5.1通过子类引用父类的静态字段,只会触发父类的初始化。
5.2定义数组对象,不会触发该类的初始化
5.3常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类初始化
5.4通过类名获取Class对象,不会触发
5.5通过Class.for Name加载指定类时,如果指定参数initialize为false,也不会触发类初始化,这个参数就是要告诉虚拟机是否进行初始化
5.6通过ClassLoader默认的loadClass方法,也不会触发
13、类加载器
把加载动作放到了JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了三种类加载器:
1.启动类加载器(BootStrap CLassLoader):用来加载Java的核心类,负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别如rt.jar)的类,由C++实现。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
2.扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME\lib\ext目录中的,或者通过java.ext.dirs系统变量指定路径中的类库。由Java语言实现,父类加载器为Null
3.系统类加载器(System Classloader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
加载过程:
JVM通过双亲委派模型进行类的加载,当然我们也可以继承java.lang.ClassLoader实现自定义的类加载器。
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终的加载任务都会传递到顶层的启动类加载器,之后当父类加载器无法完成加载任务时,才会尝试自己执行加载任务,成功则返回java.lang.Class对象,失败一般会抛出ClassNotFoundException异常。
好处时不管哪个加载器加载这个类,最终都是委托给祖宗启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同一个对象。
14、JVM回收算法以及回收器?CMS采用那种回收算法?使用CMS怎样解决内存碎片?
1.垃圾回收算法:
1.1标记-清除算法:将垃圾回收分为两个阶段:标记阶段和清楚阶段。在标记阶段首先是通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。但这个算法会存在大量的空间碎片,因为回收的空间不是连续的,这样给大对象分配内存的时候可能会提前触发full gc。
1.2复制算法:将现有内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的对象,交换两个内存的角色,完成垃圾回收。现在的商业虚拟机都采用这种收集算法来回收新生代,它是将内存分为一块较大的Eden和两块较小的Survivor空间,每次使用其中一块Survivor和Eden。当回收时,将Eden和suirvivor中还存活的对象一次性拷贝到另一个survivor中,然后清理掉Eden和使用过的Survivor空间。当Survivor空间不够的时候,就需要老年代出场了。
1.3标记整理:复制算法的高效性时建立在存活对象少,垃圾对象多的情况下。新生代常用。而老生代用的是标记-压缩算法。他在标记-清除算法的基础上做了一些优化。首先也是从根节点开始多所有可达对象做一次标记,但之后,将所有的存活对象压缩到内存的一端,之后清理边界外的所有空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
1.4增量算法:基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集的线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。但线程切换和上下文转换的消耗,总体成本上升,吞吐量下降。
2.垃圾回收器
2.1Serial收集器:是最古老的收集器,它的缺点就是当他想回收垃圾的时候,就必须停止用户的所有进程,即stop the world。到现在为止,他依然是虚拟机运行在client模式下的默认新生代收集器。与其他的收集器比较,对于限定在单个CPU的运行环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程垃圾回收效率。
Serial Old是Serial收集器的老年代版本,同样是单一线程收集器,使用标记整理的算法。这个收集器的主要意义也是被Client模式下的虚拟机使用。在Server模式下,他的两大主要用途:一个是在JDK1.5以前的版本中和ParallelScanvenge收集器搭配使用,还有一个就是作为CMS收集器的后背预案,在并发收集Concurrent mode Failure的时候使用。主要是通过指定-UseSerialGC参数,使用Serial + Serial Old的串行收集器组合进行内存回收。
2.2ParNew收集器:它是Serial收集器新生代的多线程实现,在垃圾回收的时候依然会stop the world,只不过就是运行多条进程进行垃圾回收。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果。当然随着CPU的数量增加,他对于GC时系统资源的利用还是很有好处的。它默认开始的线程数量和CPU的数量相同,可以通过-XX:ParallelGCThreads参数来限制垃圾回收的线程数。-UseParNewGC:打开次开关后,使用ParNew + Serial Old的收集器组合进行内存回收,这样新生代使用并行收集器,老年代使用串行收集器。
2.3Parallel Scavenge收集器:采用复制算法的多线程新生代垃圾回收器。他所关注的目标是吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。停顿时间越短就越适合需要与用户交互的程序,良好的反应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快的完成程序的运算任务,只要是和在后台运算而不需要太多交互的任务。Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程和标记整理算法。在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。-UseParallelGC:虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge +Serial Old的收集器组合进行内存回收。-UseParallelOldGC:打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收。
2.4CMS收集器
CMS(Concurrent Mark Swep)收集器是个比较重要的回收器,现在应用非常广泛,CMS是一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。
CMS收集器是基于标记清除算法实现的:收集过程四个步骤:初始标记(initial mark);并发标记(concurrent mark);重新标记(remark);并发清除(concurrent sweep)
注意初始标记和重新标记还是会stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。
2.5G1收集器
G1收集器是一款面向服务端应用的垃圾收集器。Hotspot团队赋予他的使命是在未来替换掉JDK1.5中发布的CMS收集器,与其他GC收集器相比它的特点有:
2.5.1并行与并发:G1能够更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
2.5.2分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾收集器的配合就可以独自管理整个GC堆。
2.5.3空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。
2.5.4可预测的非停顿:这个G1相比于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
3.CMS:
采用的是标记清除算法,解决内存碎片的问题办法就是可以让CMS在进行一定次数的Full GC(标记清除)的时候再进行一次标记整理算法,提供了两个参数来控制:-XX:UseCMScompactAtFullCollection -XX:CMSFullGCBeforeCompaction = 5
也就是说CMS在进行五次full GC之后进行一次标记整理算法,从而可以控制老年代的碎片在一定的数量以内,甚至可以配置CMS在每次FullGC的时候都进行内存的整理。