JVM
1. 内存模型以及分区,需要详细到每个区放什么。
1:主内存,工作内存,主内存存储对象的变量,各个线程修改变量都在工作内存中实现。线程之间不能跨过主内存去操作另一个线程的工作内存。必须要通过主内存。
2:运行时数据区域:虚拟机栈,本地方法栈,程序计数器,堆,方法区
① 程序计数器:线程私有的,他是一块较小的内存空间,他相当字节码于解释器中的指针,也就是该内存存放下一条即将执行指令的地址。字节码解释器就是通过改变 这个计数器的值来选择下一条即将执行的指令。每一个线程都有一个程序计数器(内存),这样线程切换的时候就能找到自己各个线程各自即将执行的下一条指令。 所以说是线程私有的。
②java虚拟机栈:线程私有的,每一个方法在执行的时候就会创建一个栈帧来存放方法的局部变量,操作数栈,返回地址等,当方法执行完成的时候就释放该栈帧。
栈帧:虚拟机栈中是一栈帧为单位存储的,所以一个虚拟机栈中有很多栈帧,每一个栈帧中分为:局部变量区(存放方法的参数和局部变量),操作数栈,方法的返回地址,动态链接(一般解析解阶段是将部分符号引用转换成直接应用(类加载),而动态链接是另外一部分的符号引用转换成直接引用(运行时))
③本地方法栈:线程私有,本地方法指的是那种不是用java语言写的方法,java虚拟机栈只针java方法,而不是本地方法。hotspot虚拟机支持别的语言写的方法在虚拟机上运行,本法方法栈和java虚拟机栈一样。只是他们服务的对象不一样而已,一个为java方法服务,一个为native方法服务。
④java堆:线程共享的,不过也可能为多个线程分配私有的buffer,也就是每个线程有自己的缓存器,java堆可以是物理上连续的,也可以是不连续的。java堆是垃圾回收器管理的主要区域,所以也叫gc堆。java堆可以分为:新生代和老年代
⑤方法区:线程共享的,可以理解为gcc中所所说的静态区,不过也不是确切的准确,因为在hotspot虚拟机中他存放的是类中静态变量和常量(注意是常量哦)。因为他能存储常量,所以还有存储常量的区域有一个特别的名称,叫做常量池(包括引用和基本数据类型的常量),方法区并不是堆,这一点和静态区很相似。所以别名叫non-heap,java堆中可以选择不实现gc回收,但是实际上呢还是会的,只能说垃圾回收器在这个区域不活跃而已,但是回收都是回收常量池中的常量,而不是静态变量。可以称为永久代。
⑥运行时常量池:他是方法区的一部分,但是和方法区的常量池有区别,他存放的常量是在运行时产生的,而不是编译时产生的。注意与普通方法区的区别
2. 堆里面的分区:Eden,survival from to,老年代,各自的特点。
1:Eden区的对象都是朝生夕死,发生minor gc的时候会清除eden区和survival区的,把存活的对象移到另一个Survival区,该survial区由老年代保证。当在年轻代中对象经过多次minor gc以后还存活,达到老年代的年纪,就会移动到老年代,还有就是大对象在年轻代无法存储,直接转到老年代,还有可能因为担保而进入老年代的
3. 对象创建方法,对象的内存分配,对象的访问定位。
1对象的创建包括三步骤:①当遇到new命令的时候,会在常量池中检查该对象的符号引用是否存在,不存在则进行类的加载,否则执行下一步②分配内存,将将要分配的内存都清零。③虚拟机进行必要的设置,如设置hashcode,gc的分代年龄等,此时会执行<init>命令在执行之前所有的字段都为0,执行<init>指令以后,安装程序的意愿进行初始化字段。
2:对象的内存分配:包括对象头,实例数据,对齐填充
①对象头:包括对象的hascode,gc分代年龄,锁状态标等。
②实例数据:也就是初始化以后的对象的字段的内容,包括父类中的字段等
③对齐填充:对象的地址是8字节,虚拟机要求对象的大小是对象的整数倍(1倍或者两倍)。因此就会有空白区。
3:对象的访问:hotspan中 是采用对象直接指向对象地址的方式(这样的方式访问比较快)(还有一种方式就是句柄,也就是建一张表维护各个指向各个地址的指针,然后给指针设置一个句柄 (别名),然后引用直接指向这个别名,就可以获得该对象,这种的优势就是,实例对象地址改变了,只要修改句柄池中的指针就可以了,而不用引用本身不会发生 改变)。
4. GC的两种判定方法:引用计数与引用链。
1:引用计数:给一个对象设置一个计数器,当被引用一次就加1,当引用失效的时候就减1,如果该对象长时间保持为0值,则该对象将被标记为回收。优点:算法简单,效率高,缺点:很难解决对象之间的相互循环引用问题。
2:引用链(可达性分析):现在主流的gc都采用可达性分析算法来判断对象是否已经死亡。可达性分析:通过一系列成为GC Roots的对象作为起点,从这些起点向下搜索,搜索所走过的路径成为引用链,当一个对象到引用链没有相连时,则判断该对象已经死亡。
3:可作为gc roots的对象:虚拟机栈(本地方法表)中引用的对象(因为在栈内,被线程引用),方法区中类静态属性引用的对象,方法区中常量引用的(常量存放在常量池中,常量池是方法区的一部分)对象,native方法引用的对象
4:引用计数和引用链是只是用来标记,判断一个对象是否失效,而不是用来清除。
5. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?
1:标记清除:直接将要回收的对象标记,发送gc的时候直接回收:特点回收特别快,但是回收以后会造成很多不连续的内存空间,因此适合在老年代进行回收,CMS(current mark-sweep),就是采用这种方法来会后老年代的。
2:标记整理:就是将要回收的对象移动到一端,然后再进行回收,特点:回收以后的空间连续,缺点:整理要花一定的时间,适合老年代进行会后,parallel Old(针对parallel scanvange gc的) gc和Serial old就是采用该算法进行回收的。
3:复制算法:将内存划分成原始的是相等的两部分,每次只使用一部分,这部分用完了,就将还存活的对象复制到另一块内存,将要回收的内存全部清除。这样只要进行少量的赋值就能够完成收集。比较适合很多对象的回收,同时还有老年代对其进行担保。(serial new和parallel new和parallel scanvage)
优化收集方法:对复制算法的优化:并不是将两块内存分配同等大小,可以将存活率低的区域大一些,而让回收后存活的对象所占的区域小一些,不够的内存由老年代的内存来保证,这样复制算法的空闲的空间减少了。
两个survival区域的是为了减少风险率,有一个survivor区要参与回收,也要参与存储,只要只有10%的空间浪费,同时也减少对老年代的依赖。
6. GC收集器有哪些?CMS收集器与G1收集器的特点。
1:串行的,也就是采用单线程(比较老了),分类:serial new(收集年轻代,复制算法)和serial old(收集老年代,标记整理),缺点:单线程,进行垃圾回收时暂时所有的用户线程。优点:实现简单。
2:并行的,采用多线程,对于年轻代有两个: parallel new(简称ParNew)(参考serial new的多线程版本)和parallel scavenge;parallel scavenge是一个针对年轻代的垃圾回收器,采用复制算法,主要的优点是进行垃圾回收时不会停止用户线程(不会发生stop all world)
老年代回收器也有两种:Parallel old是parallel scavenge的我老年代设计的。CMS(并发标记清除),他采用标记清除算法,采用这种的优点就是快咯,因此会尽快的进行回收,减少停顿时间。
3:高级杀手:G1收集器,年轻代和老年代通吃,最新一代的技术。面向服务器端的垃圾收集器(并行+并发的垃圾收集器)。
7. Minor GC与Full GC分别在什么时候发生?
1.Minor GC发生:当jvm无法为新的对象分配空间的时候就会发生minor gc,所以分配对象的频率越高,也就越容易发生minor gc。
2.Full GC:发生GC有两种情况,①当老年代无法分配内存的时候,会导致MinorGC,②当发生Minor GC的时候可能触发Full GC,由于老年代要对年轻代进行担保,由于进行一次垃圾回收之前是无法确定有多少对象存活,因此老年代并不能清除自己要担保多少空间,因此采取采用动态估算的方法:也就是上一次回收发送时晋升到老年代的对象容量的平均值作为经验值,这样就会有一个问题,当发生一次Minor GC以后,存活的对象剧增(假设小对象),此时老年代并没有满,但是此时平均值增加了,会造成发生Full GC
8. 几种常用的内存调试工具:jmap、jstack、jconsole。
9. 类加载的五个过程:加载、验证、准备、解析、初始化。
1:加载:加载有两种情况,①当遇到new关键字,或者static关键字的时候就会发生(他们对应着对应的指令)如果在常量池中找不到对应符号引用时,就会发生加载 ,②动态加载,当用反射方法(如class.forName(“类名”)),如果发现没有初始化,则要进行初始化。(注:加载的时候发现父类没有被加载,则要先加载父类)
2:验证:这一阶段的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全(虽然编译器会严格的检查java代码并生成class文件,但是class文件不一定都是通过编译器编译,然后加载进来的,因为虚拟机获取class文件字节流的方式有可能是从网络上来的,者难免不会存在有人恶意修改而造成系统崩溃的问题,class文件其实也可以手写16进制,因此这是必要的)
3:准备:该阶段就是为对象分派内存空间,然后初始化类中的属性变量,但是该初始化只是按照系统的意愿进行初始化,也就是初始化时都为0或者为null。因此该阶段的初始化和我们常说初始化阶段的初始化时不一样的
4:解析:解析就是虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用其实就是class文件常量池中的各种引用,他们按照一定规律指向了对应的类名,或者字段,但是并没有在内存中分配空间,因此符号因此就理解为一个标示,而在直接引用直接指向内存中的地址。
5:初始化:简单讲就是执行对象的构造函数,给类的静态字段按照程序的意愿进行初始化,注意初始化的顺序。(此处的初始化由两个函数完成,一个是<clinit>,初始化所有的类变量(静态变量),该函数不会初始化父类变量,还有一个是实例初始化函数<init>,对类中实例对象进行初始化,此时要如果有需要,是要初始化父类的)
10. 双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
类 加载器的工作过程:如果一个类加载器收到类类加载的请求,他首先不会自己去加载这个类,而是把类委派个父类加载器去完成,因此所有的请求最终都会传达到顶 层的启动类加载器中,只有父类反馈无法加载该类的请求(在自己的搜索范围类没有找到要加载的类)时候,子类才会试图去加载该类。
11. 分派:静态分派与动态分派。
1:静态分派和动态分派都是多态的内容,多态的实现依赖于编译阶段和运行时阶段:在编译阶段主要表现在静态分派,
2:静态分派就是通过静态类型和方法参数个数来选择哪一个方法版本,这就是主要体现了方法的重载;因为他在编译的时候就能确定调用哪一个函数,所以叫静态分派。
3:在运行时阶段体现在动态分派(动态绑定),也就是当一个父类引用指向子类对象,通过该父类引用去调用一个该方法,由于在编译阶段生产的调用函数代码的字节码指向的是父类(静态类型)被调用方法,并不知道具体要去调用哪一个实际类型的方法,因 此会发生这样一个过程,虚拟机找到操作数栈中位于栈顶获取该操作数的指所指向的类,然后到常量池中去搜索与被调用的方法匹配的方法名和描述符,如果找到, 就进行权限校验(校验失败就抛出异常),如果可以访问,则返回该方法的符号引用,并转换成直接引用,调用该执行,如果找不到就到父类中去找,然后重复上面 动作,最后找不到就抛出异常。
4: 对动态绑定的优化:由于要去常量池中搜索每一类的方法名和描述符,因此效率比较低,所以最后进行了优化,就是在方法区为每一类维护一张虚方法表或者接口方 法表(虚表中存放了该方法的实际入口地址),让该类的所有方法都维护进去(包括父类的方法),因此要查找方法名的时候,直接去该虚表中去搜索到该方法名对 应的直接地址然后执行。对于没有被重写的方法,直接存放父类的入口地址,如果该方法被重写,在存放子类的方法入口地址。
JVM过去过来就问了这么些问题,没怎么变,内存模型和GC算法这块问得比较多,可以在网上多找几篇博客来看看。