JVM
JVM体系结构主要由4部分组成:
1)类加载器:在JVM启动时类运行时将需要的class加载到JVM中。
2)执行机器:负责执行class文件包含的字节码指令。
3)内存区:又成运行数据区,下面详解。
4)本地方法调用:调用C/C++本地方法并返回。
内存区/运行数据区域:主要包括程序计数器、java虚拟机栈,本地方法栈、java堆、方法区、运行常量池。
程序计数器:是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,属于线程私有。
Java虚拟机栈:属于线程私有,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈:本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。
Java堆:Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(类加载过程中第一部分“类加载时会把字节码加载到方法区”)。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。
运行常量池:运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。比如String创建。
2.虚拟机的类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的JAVA类型,这就是虚拟机的类加载机制。类加载的生命周期包括:加载Loading, 验证Verification, 准备Preparation, 解析Resolution, 初始化Initialization, 使用Using和卸载Unloading. 除解析阶段外,其他几个阶段的顺序都是固定的。解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持JAVA语言的运行时绑定(动态绑定/晚期绑定)虚拟机规范严格规定了有且只有四种情况必须对类进行初始化(加载,验证,准备自动在之前开始)
1)遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行初始化,则先
2)初始化。这4个字节码常见的出现场景是:使用new关键字实例化对象的时候,读取或设置静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
3)反射调用时
4)初始化一个类时,如果其父类还未初始化,则先出发父类初始化。当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。双亲委派模型执行过程:一个类收到类加载请求首先不会去加载这个类,而是把这个请求委派给父类类加载器去完成,每一层的类加载都是这样,因此所有的类加在最终都传递到启动类加载器,只有父类无法完成请求时之类才尝试自己加载。
类加载的几种方式
1)命令行启动应用时候由JVM初始化加载,加载含有main的主类。
2)通过Class.forName()方法动态加载类,默认会执行初始化块。如果指定了ClassLoader,则不会执行初始化块。
3)通过ClassLoader.loadClass()方法动态加载类,不会执行初始化块。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
加载+验证+准备+解析+初始化
1、“加载”阶段,虚拟机需要完成以下三件事情:
(1)通过一个类的权限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
2、验证
(1)文件格式验证–验证class文件格式规范,例如:class文件是否已魔术0xCAFEBABE开头 ,主、次版本号是否在当前虚拟机处理范围之内等。
(2)元数据验证(语义分析)–这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类是否继承了不允许被继承的类(被final修饰的)、如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。
(3)字节码验证–进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为.如:保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体以外的字节码命令上。
(4)符号引用验证–符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。
3、准备
准备阶段是正式为类变量分配内存并设置变量初始值的阶段,这些内存都将在方法区中进行分配。
注意:
(1)进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中
(2)初始值是指数据类型的零值,例如:
public static int value = 123;
那么变量value在准备阶段过后的值是0,而不是123,因为这时候尚未开始执行任何java方法。
再看:public static final int value = 123;
final修饰的字段,子啊准备阶段虚拟机就会根据赋值,value的值为123.
4、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
(1)类或接口的解析
(2)字段解析:首先解析出类的方法表的class_index项中索引的字段所属的类或接口的符号引用,然后继续搜索:
先在类本身中查找字段
再在类的实现的接口中查找字段
然后在类的父类中查找字段
否则,查找失败
(3)类方法解析:首先解析出类的方法表的class_index项中索引的方法所属的类或接口的符号引用,然后继续搜索:
类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的是个接口,直接报错。
在类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有直接返回
在类实现的父类中查找是否有简单名称和描述符都与目标相匹配的方法,如果有直接返回
在类实现的接口列表及他们的父接口中查找是否有简单名称和描述符都与目标相匹配的方法,如果有直接报错
否则,查找失败
(4)接口方法解析:首先解析出类的方法表的class_index项中索引的方法所属的类或接口的符号引用。正好与接口相反。
5、初始化
初始化阶段才真正开始执行类中定义的java程序代码
初始化阶段是执行类构造器()方法的过程。在以下四种情况下初始化过程会被触发执行:
1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。
2.使用java.lang.reflect包的方法对类进行反射调用的时候
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化
4.jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
在准备阶段,变量已经赋过一次系统要求的初始值,二在初始阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
热部署
对于Java应用程序来说,热部署就是在运行时更新Java类文件。
比如,我们在使用Tomcat或者Jboss等应用服务器开发应用时,我们经常会开启热部署功能。热部署,简单点来说,就是我们将打包好的应用直接替换掉原有的应用,所以,要实现一个支持热部署的应用,我们可以对每一个用户自定义的应用程序使用一个单独的ClassLoader进行加载。然后,当某个用户自定义的应用程序发生变化的时候,我们首先销毁原来的应用,然后使用一个新的ClassLoader来加载改变之后的应用。而所有其他的应用程序不会受到一点干扰。
4.虚拟机垃圾回收机制及算法
5、jvm性能调优都做了什么。
6、写代码分别使得JVM的堆、栈和持久代发生内存溢出(栈溢出)防止内存溢出手段。
7、据你了解,除了反射还有什么方式可以动态的创建对象?
(我提到了CGLIB…… 我以为他会接着问CGLIB,揪心中……,结果他没问)
2. 堆里面的分区:Eden,survival from to,老年代,各自的特点。
3. 对象创建方法,对象的内存分配,对象的访问定位。
对象内存分配是在堆中,这个对象的引用是在栈中分配,堆栈中分配的内存只是一个指向堆中的引用而已。
4.GC的两种判定方法
引用计数:通过判断对象被引用的次数(为0,则表示不可被使用),但这很难解决对象相互循环引用的问题。
根搜索算法:算法基本思路就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。在Java语言中,GC Roots包括:
1)在JVM栈(帧中的本地变量)中的引用
2)方法区中的静态引用
3)JNI(即一般说的Native方法)中的引用
4)方法区中的常量引用。
5.GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?
标记-清除: 先标记要回收的对象,再统一进行回收操作。简单方便,内存碎片化严重.
复制算法: 将堆分为2份,1份空白(A),1份存放对象(B),GC后,将B中存活对象复制到A中,然后将B变成空白。无内存碎片化, 浪费可用内存。
标记-整理: 先标记可回收对象,将存活对象都移向一端,再回收对象。无内存碎片化,充分利用可用内存。
分 代回收:将堆分为年轻代和老年代,年轻代(存活对象少)采用复制算法,老年代(对象存活长)可采用标记-清除或标记-整理。根据对象存活特性,合理使用不同回收算法,商业JVM都使用该回收算法。
新创建的对象被分在年轻代
触发垃圾回收方法
1对象没有引用
2作用域发生未捕获异常
3、程序在作用域正常执行完毕
4程序执行了System.exit()
5程序发生意外终止(被杀进程等)
分代垃圾回收
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
如图所示:
虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代:
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,会触发MinGC,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代:
Old区满后会触发FullGC在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。如果该区满了后也会触发FullGC
6.GC收集器有哪些?CMS收集器与G1收集器的特点。
既然jvm采用分代回收,那么年轻代和老年代各自使用的垃圾收集器也不一样。
年轻代使用的垃圾收集器:Serial,ParNew,Parallel Scavenge。
老年代使用的垃圾收集器:CMS, Serial Old(MSC), Parallel Old。
通用的垃圾收集器:G1。
Serial/Serial Old收集器:单线程回收,回收期间暂停其他所有工作线程。(jvm -client模式下年轻代默认的收集器)
ParNew收集器:其实就是Serial收集器的多线程版本,回收期间暂停其他所有工作线程。是运行在许多Server模式下的Young收集器,是除了Serial之外目前只有它能与CMS收集器一起工作。这种收集器会有线程交互的开销所以在单CPU甚至是双CPU环境下都不一定能有Serial收集器效果。
Parallel Scavenge收集器:和ParNew类似,但是它采用并行多线程,但其重在控制cpu吞吐量大小。
吞吐量=运行代码时间/(运行代码时间+垃圾收集时间)
Serial Old收集器:Serial老年代版本。使用单线程,标记-整理的算法。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本。使用多线程,标记-整理算法。
CMS(Concurrent Mark Sweep)收集器:以获得最短回收暂停时间为目标。这是一款真正意义的并发收集器(可允许垃圾收集线程与用户线程并行)。其主要有四个阶段:
1. 初始标记:标记GC Roots可关联的对象,会暂停用户线程。
2. 并发标记:GC Roots Tracing过程,用户线程并行。
3. 重新标记:重新标记由于在并发标记过程中用户线程导致的对象状态变化,会暂停用户线程。
4. 并发清理:清理可回收对象,用户线程并行。
优点:并发收集,降低停顿。
缺点:
1)对CPU资源很敏感
2)无法处理浮动垃圾,由于收集过程用户还在继续可能会产生新垃圾
3)是一款标记清除算法,会产生内存碎片。当然它提供了一个碎片整体过程但是无法并发停顿时间较长。
G1(Garbage First)收集器:一款先进的垃圾收集器。它针对整个堆,将其分为大小相等的区域,记录每个区占用信息,优先回收回收价值更高的区域。将Java堆划分为多个大小固定的独立区域,并且跟踪这些区域里的垃圾堆积程度,在后台维护一个优先列表,每次根据优先列表优先回收垃圾最多的区域。
7. Minor GC与Full GC分别在什么时候发生?
对于Minor GC 和 Full GC的解释: 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。MajorGC 的速度一般会比 Minor GC 慢 10 倍以上。虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。