目录
一、虚拟机的理解
虚拟机其实就是通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境下的完整计算机系统。只要是在实体计算机上能实现的功能,虚拟机基本都能实现。
JVM虚拟机主要是由类加载器、运行时数据区、执行引擎和本地方法接口四个部分组成。
它的主要特点首先可以跨平台使用,可以运行在不同的计算机系统,如Linux、MacOS等。
跨语言不管是什么编程语言经过编译转换为的字节码文件,他都能识别。
二、java如何实现跨平台机制
java的跨平台其实是因为JVM。因为不同的平台使用的JVM不一样。
通常java源代码编译后的文件为字节码文件。而JVM可以将字节码文件翻译成当前平台下的机器码运行。而我们只需要在不同平台下安装相应的JVM,就可以运行字节码文件。
三、JVM内存模型(JMM)
JVM内存区域分为两种一种为线程共享的区域,一种为线程私有的区域。
线程共享的区域有堆区和元空间。
其中堆区的话是jvm中最大的一块区域,也是存放对象实例以及数组的地方。而堆区中又分为老年代和新生代,他们的占比分别为2:1,在老年代中存放的一般是一些不容易被删除的对象,老对象,而新生代存放的则是一些新创建的对象,容易被删除的对象。在新生代中也划分为3个区域Eden区和两个相同的Survivor区,他们的占比分别为8:1:1。
元空间的话存放的是一些常量、静态变量、类信息、JIT即时编译器编译后的机器代码等。
线程私有的区域有虚拟机栈、本地方法栈、程序计数器。
虚拟机栈它是由一个个栈帧组成,每个栈帧都拥有局部变量表、操作数栈、动态连接和方法返回地址。而被调用一个方法,对应的栈帧都会被压入虚拟机栈、而当方法结束后,又会从虚拟机栈中弹出。最顶层的栈帧代表着当前正在执行的方法,这些栈帧其实就相当于方法的调用,调用一个方法一个栈帧入栈,当这个方法彻底结束,栈帧又会被弹出,回到调用它的那个栈帧中。
本地方法栈也是存放一个个栈帧,不过该栈帧所对应的方法是由native修饰的本地方法。
程序计数器则是用于记录当前线程所执行的字节码的行号,它主要由两个用处,一个是用于多线程的情况下,用于切换线程时记录当前执行的位置,方便下次执行时从当前位置继续执行。另一个则是用于实现代码的流程控制,循环、异常等,通过行号的切换实现执行位置的跳转。
四、JVM栈和堆的区别
首先堆和栈是两个不同的区域,堆是线程共享的区域,而栈是线程私有区域。
堆存放的是对象的实例和数组,而栈存放的则是一个个栈帧。而栈帧中存放是局部变量、操作数栈、动态链接和方法返回值。
五、垃圾回收是在哪个区域发生,讲一下垃圾回收?
垃圾回收发生的区域为堆和元空间,主要是在堆中,一般在堆中的新生代发生次数比较多,老年代发生概率比较低。
而当一个对象没有什么用处时,则该对象就是需要被回收的垃圾对象。JVM默认使用可达性分析算法来推断这个对象是否需要回收。(可达性分析算法其实就是定义一系列称为“GC Roots”的根对象为起始节点。对一个对象通过引用关系向上寻找,如果没有找到这个GC Roots则说明这个对象为垃圾对象)。
垃圾回收的大概过程其实就是,一般新对象都是在Eden区生成的,而当Eden区空间已满时,就会触发YGC,对新生代进行垃圾回收,将Eden区中存活的对象,复制到survivor区。在survivor区中分为两块相等的区域,为s0,s1,其中只有一块用于存放对象,而每次在YGC时,survivor区中存放对象的区域会将所有不需要回收的对象复制到另一块空区域中,将存放对象的区域清除,然后交换两块区域的使用状态,每一次交换都会使对象的年龄+1。如果Eden区要移送的对象大于survivor区的容量上限,则会直接送到老年代,一般对象的年龄到达15时也会晋升至老年代。如果在进行YGC后,Eden区还是存放不下,则会存放到老年代,如果老年代也存放不下,则会对整个堆内存进行FGC,如果还是存放不下,则会内存溢出。
然后垃圾回收是指不同的垃圾收集器通过不同的算法来对垃圾对象进行处理。
六、垃圾回收算法
垃圾回收算法主要分为三种
标记-清除算法他是先将不需要回收对象先标记起来,标记完成后,清除所有未标记的对象。如果这个区域大部分都是需要回收的对象,那么这种算法的效率就会很低下。而这个会造成空间碎片化问题,导致产生大量不连续的空间,如果有一个大的对象需要分配,就无法找到连续足够的空间。
标记-复制算法是先将空间分为完全一样大小的两块空间,每次使用只是用其中一块,在一块使用完后,先将不需要回收的对象复制到另一块中,然后清除这块空间。这个算法可以改善空间碎片化问题,但是会浪费过多的空间,使现在只能是原来一般,而且如果不需要回收的对象,如果较多化,效率会很低。
标记-整理算法也是先标记所有不需要回收的对象,然后将所有标记的对象向着一端移动,最后清除边界以外的内存。这种算法也是可以防止空间碎片化问题。
七、介绍一下你知道的垃圾收集器
Serial收集器(新生代)
采用标记-复制算法负责新生代的垃圾回收器,是一个单线程收集器。
Serial Old收集器(老年代)
采用标记-整理算法负责老年代的垃圾回收器,也是单线程收集器。
ParNew收集器(新生代)
采用标记-复制算法负责老年代的垃圾回收器,多线程收集器。
Parallel Scavenge收集器(新生代)
采用标记-复制算法负责新生代的垃圾回收器,多线程收集器,以达到一个可控制的吞吐量为目的的收集器。
Parallel Old收集器(老年代)
采用标记-整理算法负责老年代的垃圾回收器,多线程收集器,同样是注重吞吐量的一个收集器。
CMS收集器(老年代)
采用标记-清除算法负责老年代的垃圾收集器,多线程收集器,以获取最短停顿时间为目的的收集器。他是第一个实现垃圾收集线程和用户线程同时工作。
他的工作流程分为四个步骤:初始标记(快速标记一下可以通过GC roots直接关联的对象)、并发标记(用户线程和标记线程同时进行)、重新标记(停止用户线程,标记并发后遗留的记录)、并发清除(用户线程和清除线程同时进行)。
优点:可以并发收集、低停顿。
缺点:影响用户线程的执行效率。无法处理浮游垃圾。产生大量空间碎片(标记-清除算法)。
G1收集器(全代)
采用标记-整理算法面向服务器的垃圾收集器,他不严格按照分代思想进行垃圾回收。采用局部性收集的设计思想,他将堆内存划分为2048个大小相同的区域,那一块垃圾最多,对那一块进行收集,使得回收收益最大。
他的工作流程也分为四个步骤:初始标记、并发标记、最终标记(与重新标记一样)、筛选回收(负责更新每个区域的统计数据,对其回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择多个区域进行回收,也可以通过标记-复制算法将哪一个区域存活的对象复制到另一个空区域,然后清除这个区域)。
八、cms和g1的区别
首先使用范围不一样,CMS收集器是用于老年代的收集器,可以配合Serial和PaeNew收集器使用。G1收集器则可以用于老年代也可以用于新生代。不需要结合其他收集器使用。
STW(Stop The World)的时间,CMS收集器是以最小停顿时间为目的的收集器,G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)。
垃圾回收算法不同,CMS收集器使用的是标记-清除算法,容易产生内存碎片。而G1收集器使用的是标记-整理算法,降低了内存空间碎片。
垃圾回收的过程不同,CMS收集器依次是初始标记、并发标记、重新标记、并发清除。而G1收集器则是初始标记、并发标记、最终标记、筛选回收。
九、谈一谈jvm常见命令与参数
jvm常见命令:
jps:查看所有java进程。
jstat:查看jvm各方面的运行数据。
jinfo:查看jvm各项信息。
jstack:生成虚拟机当前时刻的线程快照。
jvm常见参数:
-Xms:设置初始java堆大小
-Xmx:设置最大java堆大小
-Xmn:新生代大小
-Xss:设置java线程栈大小
十、哪些对象可以作为GC Roots的根对象
虚拟机栈中引用的对象。
本地方法栈中引用的对象。
元空间中类静态属性和常量所引用的对象。
十一、说一下类加载过程(什么是类加载机制?)
在JVM虚拟机实现规范中,通过ClassLoader类加载器把*.class字节码文件(文件流)加载到内存,并对字节码文件内容进行验证、准备、解析和初始化,最终形成可以被虚拟机直接使用的java.lang.Class对象,这个过程被称作类加载。
加载:先通过类的完全限定名获取定义该类的二进制字节流。然后将该字节流的静态存储结构转换为元空间的运行时存储结构。在内存中生成一个代表该类的Class对象,作为元空间中该类各所数据的访问入口。
验证:确保Class文件的字节流包含的信息符合当前虚拟机的要求。
准备:在元空间中为静态变量分配内存,并设置初始值。一般设置为0,如果是常量则设置为表达式定义的值。
解析:将常量池的符号引用替换为直接引用。
初始化:虚拟机执行类构造器的过程。
十二、类加载器分类,每个都干了什么
启动类加载器:用于加载java核心类库,无法被java程序直接引用。
扩展类加载器:用于加载java扩展库,java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类。
应用程序类加载器:用于加载用户类路径(ClassPath)上的所指定的类库。
用户自定义加载器:用户可以通过继承的方式来实现自己的类加载器。
十三、双亲委派模型
双亲委派模型是类加载器之间一种层次关系,除了最顶层的启动类加载器之外,其他的类加载器都需要有自己的父类加载器。
双亲委派模型的实现原理:先检查类是否已经被加载过,如果没有则让父类加载器去加载,如果父类加载器加载失败,则会抛出ClassNotFoundExceptin,此时尝试自己去加载。
十四、新生代为什么要分为Edem区和survivor区
如果没有survivor区的话,在新生代进行垃圾收集后存活的对象就会进入老年代,这样的话老年代就会很快占满,进行非常耗时的fgc。
因为在新生代的标记-复制算法,他的基本思路是把新生代分为两个大小相同的区域,只是用其中一块区域,当区域大小不足以分配对象时,就会促发ygc,将存活的标记起来,复制到另一个空区域,然后清除当前区域,这样的话每次只能使用一半区域,对内存的使用效率太低了!
所以设置出Eden和两个survivor区,比例设为8:1:1,这样的话,在第一次进行一次垃圾收集时,先将Eden区存活的对象依次复制到其中一块survivor区,然后进行清理整个Eden区,当后面进行垃圾收集时,就会将Eden区和survivor区的存活对象依次复制到另一个空的survivor区中,然后清除当前Eden和当前survivor区,这样的话,可以始终保存一块空的survivor区,循环利用这三块空间,这样的话就可以将内存利用率变为原来的90%,而不是一半了。
十五、JVM调优的方式
JVM调优主要是为了减少GC频率和Full GC次数,STW时间和次数。
一般在通过System.gc()方法、老年代空间不足、堆中产生大对象、老年代中连续空间不足时会发生Full GC。
一般可以通过JVM参数和JVM工具进行调优。
注:本篇文章都是我自己的理解,可能用词和语句不够严谨,如有错误请评论指正,谢谢!(持续更新中......)