引言:JVM常见面试题:
1、请谈谈你对JVM的理解?java8的虚拟机有什么更新?
2、什么是OOM?什么是StackOverflowError?有哪些方法分析?
3、JVM的常用调优参数你知道哪些?
4、谈谈JVM中,对类加载器你的认识?
5、JVM内存模型以及分区,需要详细到每个区放什么
6、堆里面的分区:Eden、survival from to、老年代各自的特点
7、GC的三种收集算法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方
8、Minor GC与Full GC分别在什么时候发生
一、类装载器(ClassLoader):
负责加载class文件,class文件在文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
启动类加载器加载的是java程序最底层的一下jar包,放在rt.jar,java的版本在不断的更迭,新的jar包由扩展类加载器进行加载。应用程序类加载器则是加载我们在程序中自己写的类。
public class Obb {
public static void main(String[] args) {
Object o = new Object();
System.out.println("o:" + o.getClass().getClassLoader());
Obb ob = new Obb();
System.out.println("ob:" + ob.getClass().getClassLoader());
}
}
如上所示一段代码,Object是jdk自带的,所以它的类加载器应该是根加载器,而ob是我自己定义的,他的加载器理所当然的应该是应用程序类加载器,根加载器返回的是null。
二、本地方法栈和本地方法接口
三、程序计数器
四、方法区(Method Area):
供各线程共享时的内存区域。它存储了每一个类的结构信息
,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的时规范,在不同虚拟机里头实现时不一样的,最典型的就是永久代(java8以前)和元空间(java8以后)。
实例变量时存放在堆内存的,和方法区无关。
五、栈(Stack):
栈也叫栈内存,主管java程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存种分配。
栈管运行,堆管存储。
栈存储什么?
栈帧种主要保存三类数据:
本地变量:输入参数和输出参数以及方法内的变量;
栈操作:记录出栈,入栈的操作;
栈帧数据:包括类文件、方法等等。
/**
* 如下所示,main方法作为程序的入口,首先被作为栈帧压入栈的底部,
* 然后main方法又调用了sayHello方法,将此方法栈帧压入栈中,
* 而sayHello方法又不停的自己调用,每次调用方法都会形成一个栈帧压入栈,
* 而栈的大小是有限的,所以最后会导致栈溢出 StackOverflowError
*注意StackOverflowError是错误而不是异常
*/
public class StackDemo1 {
public static void main(String[] args) {
System.out.println("1");
sayHello();
System.out.println("3");
}
public static void sayHello() {
System.out.println("hello");
sayHello();
}
}
在栈中存着对象的引用,指向堆中,在栈中每一个引用存储着的是对象的地址,都会在堆中对应着一个类元数据地址,而堆中的类元数据地址会指向方法区,因为方法区存储着这个对象的结构数据,也就是一个对象的模板。
六、堆(Heap):
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。堆内存分为三部分:
PS:
幸存者0区又称为S0区/From区
幸存者1区又称为S1区/To区
java堆从GC的角度上还可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代。
元空间是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
七、JVM的参数调节
如上图所见,java7和java8对于jvm参数的调整,实际上相差不大,
-Xms表示堆内存的起始值,默认是物理内存的1/64
-Xmx表示堆内存的最大值,默认是物理内存的1/4
-Xmn表示堆中young区设置的值
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
因此服务器一般设置-Xms、-Xmx相等
以避免在每次GC 后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。
(java8以前的永久代)
-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;
XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
在java8以后,永久代已经被移除,被元空间所代替,元空间本质上和永久代类似。区别在于永久代使用的是JVM的堆内存,但是java8以后元空间并不在虚拟机中而是使用本机物理内存。因此,默认情况下,元空间的大小受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由maxPerSize控制,而是由系统的实际可用空间来控制。
public class jvmSwitch {
public static void main(String[] args) {
//返回JVM试图使用的最大内存量,默认的是系统内存的1/4左右 3616MB/16G
long maxMemory = Runtime.getRuntime().maxMemory();
//返回JVM的内存总量
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("MAX_MEMORY=" + maxMemory + "(字节)" +
(maxMemory / 1024 / 1024) + "MB");
System.out.println("TOTAL_MEMORY=" + totalMemory + "(字节)" +
(totalMemory / 1024 / 1024) + "MB");
}
}
八:GC算法
分代收集算法!!!
1、GC是分代收集算法:次数上频繁收集的是Young区、次数上较少收集的是old区,基本不动的是元空间。
JVM在进行GC时,并非每次都对三个内存一起回收的,大部分时候回收的都是新生代,因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种时全局GC(major GC or Full GC)
Minor GC和Full GC的区别:
minorGC:只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
majorGC:指发生在老年带的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但是不是绝对的),Major GC的速度一般要比Minor GC慢上10倍以上(因为老年代比新生代大啊!)。
四大算法:引用计数法、复制算法、标记清除法、标记压缩法
2、引用计数法
引用计数法如上所示,在程序中有一个引用,就+1,但是可能会出现A引用B,B引用A的情况,这样的话循环引用较难处理。
2、复制算法
年轻代中的GC,主要靠的是复制算法,年轻代中分为一个Eden和两个Survivor,默认比例是8:1:1。一般情况下新创建的对象会被分配到Eden区(一些大的对象特殊处理)。因为年轻代中的对象基本上都是朝生夕死,所以年轻代的垃圾回收算法使用的是复制算法,复制算法的思想就是将内存分为两块,每次用其中一块,当这一块内存用完,就将还活着的对象复制到另一块上面,复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为"From"的Survivor区,Survivor区"To"是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到"To",而在“From”区中,仍会存活的对象就会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到老年代,没有达到阈值的对象会被复制到"To"区域。经过这次"From",新的"From"就是上次GC前的"To",不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区域被填满,"To"区被填满之后,会将所有对象移动到老年代中。
缺点:
1、浪费了一半的内存
2、如果对象存活率高,可以极端一点,假设是100%存活,那么我们需要将所有的对象都复制一遍,并将所有引用地址重置一遍,复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变得不可忽视。所以要使用复制算法,存活率必须非常低才行,更重要的是,要克服50%内存的浪费。
3、标记清除算法
1、优缺点
优点:不需要额外的空间
缺点:两次扫描,耗时严重;会产生内存碎片。
当程序运行时,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。
缺点解释:首先,他的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。其次,主要缺点则是这种方式清理出来的空间内存事不连续的,因为死亡对象都是随机出现在内存的各个角落的,现在把它们清除之后,内存的布局就会乱七八糟。为了应对这一点,JVM必须维持一个内存的空间列表,这又是一种开销,而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
4、标记整理法/标记压缩法(全称:标记-清除-整理算法)
老年代一般是由标记清除或者是标记清除+标记整理
优缺点:
优点:没有内存碎片
缺点:需要移动对象的成本(耗时长)
效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址,从效率上来讲,标记/整理算法要低于复制算法。
5、总结:
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法
可以看出效率上来讲,复制算法是最快的,但是浪费了太多的内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
没有最优的算法,只有最合适的算法。======>>>>分代收集算法
年轻代
年轻代的特点是区域相对老年代较小,对象存活率低。这种情况用复制算法回收整理,速度是最快的。
老年代
这种情况存在大量存活率高的对象,复制算法变得不合适,一般是由标记清除与标记整理的混合实现。