下面对关于jvm的问题做一个总结和整理,分享给大家,方便大家一起学习。中间有说得不对的地方,还请大家多多批评指正。
1.什么是JVM及其工作原理
答:JVM是用于执行java程序的虚拟机,是java跨平台特性的基础,其工作原理是模拟处理器,堆栈,寄存器等,还有相应的系统指令.
2. 描述一下JVM 加载class文件的原理机制?
答:由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
(1)加载
这个是由类加载器执行的。该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的),并从这些字节码中创建Class对象。
(2)链接
a) 验证 主要验证class文件的格式是否正确,语法是否正确,字节码是否正确,二进制是否兼容
b) 准备 为类的静态变量分配空间及设置默认初始值。
c) 解析:将class文件中符号引用转变成直接引用。
(3)初始化
如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
(参考《java编程思想》第14章 类型信息)
java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载七个阶段。
3.JVM中的类加载器都有哪些?
答:类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从JDK 1.2开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
a)Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
b)Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
c)System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
4.解释内存中的栈(stack)、堆(heap)和静态存储区的用法。
答:(1) 堆 用于存放通过new关键字和构造器创建的实例
(2) 栈 用于存放基本数据类型的变量,对象的引用以及函数的调用线程
(3) 静态存储区(方法区):用于存放常量,静态变量,加载的类信息,字面量,符号引用
除了这3个区域外,JVM内存区域还划分了 计数器,有些虚拟机甚至会将栈更具体的划分成 本地方法栈和虚拟机栈,而方法区还划分运行常量池,用于存放编译时生成的字面量和符号引用
String str = new String(“hello”);
上面的语句中str放在栈上,用new创建出来的字符串对象放在堆上,而“hello”这个字面量放在静态存储区。
补充:较新版本的Java中使用了一项叫“逃逸分析“的技术,可以将一些局部对象放在栈上以提升对象的操作性能。
下面给大家看一下jvm的内存模型:
5.Heap 堆内存又可以细分成什么?
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
Young Generation Space 新生区 Young
Tenure generation space 养老区 Old
Permanent Space 永久存储区 Perm
下面给大家看一下堆内存示意图:
新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
养老区
养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。
永久区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
Jdk1.6及之前: 常量池分配在永久代
Jdk1.7: 有,但已经逐步“去永久代”
Jdk1.8及之后: 将最初的永久代取消了,由元空间取代。目的是将HotSpot与JRockit两个虚拟机标准
6.GC 是什么?为什么要有GC?
答:GC是垃圾回收机制,是JVM对内存进行管理的方式。其基本原理就是对对象的引用进行监控,当发现对象不可达时, GC将负责回收所有"不可达"对象的内存空间。
GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。
7.垃圾回收算法都有哪些?
答:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。
1)标记清除算法:首先标记处所有需要回收的对象,在标记完成后统一进行回收。
缺点:标记的过程效率不高、标记清除之后产生大量不连续的内存碎片,当需要申请大块连续内存空间时,无法找到。
2)复制算法:将内存按容量费为大小相等的两块区域,每次只使用其中的一块,当一块内存用完了,就将还存活的对象复制到另一块内存上面,然后吧使用过的那块内存统一清理掉。
缺点:每次只能使用总内存容量的一半。在对象存活较多的情况下会进行大量复制操作,效率底下。
3)标记整理算法:和标记清除算法一样,先对死亡对象进行标记,然后将存活对象向一端移动,然后直接清理掉边界以外的内存。
4)分代收集算法:根据对象存活周期的不同,将内存划分为新生代和老年代,新生代使用复制算法,老年代使用标记清除或标记整理算法进行垃圾收集。在垃圾收集过程中,可能会将对象移动到不同区域:
· 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
· 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
· 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。
8. StackOverflowError和OutOfMemoryError,谈谈你的理解
答:如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
OutOfMemoryError表示堆溢出,StackOverFlowError表示栈溢出
9一般什么时候会发生GC?如何处理?
答:Java中的GC会有两种回收:年轻代的M inor GC,另外一个就是老年代的Full GC;新对象创建时如果伊甸园空间不足会触发MinorGC,如果此时老年代的内存空间不足会触发Full GC,如果空间都不足抛出OutOfMemoryError。
10.jvm中新生代与老年代的区别?
答:新生代对象存在的生命周期较短,老年代对象生命周期长
采用的垃圾回收机制算法不一样,新生代采用的是复制算法,老年代采用的是标记-清除算法
11.JVM常见启动参数都有哪些?
- -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
- -Xmn — 堆中年轻代的大小
- -XX:-DisableExplicitGC — 让System.gc()不产生任何作用
- -XX:+PrintGCDetails — 打印GC的细节
- -XX:+PrintGCDateStamps — 打印GC操作的时间戳
- -XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
- -XX:NewRatio — 可以设置老生代和新生代的比例
- -XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
- -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最 大值
- -XX:TargetSurvivorRatio:设置幸存区的目标使用率
12.Java 中会存在内存泄漏吗,请简单描述。
答:理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收也会发生内存泄露。一个例子就是hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象。
13.描述一种虚拟机栈内存溢出的场景,有什么有效的解决方法?
答:一种是当线程在运行时,调用了的次数过多,会导致线程栈变的很深,如果大于虚拟机所允许的最大深度,就会发生StackOverflowError。第二种方法递归没终止条件。
解决方法:每新建一个线程时,会分配给这个线程一个栈内存初始值,最大的大小可通过 -Xss 来设置。线程占有的栈内存大小,通过不断执行方法,生成局部变量等操作,栈桢不断增加,该线程的栈内存也不断被使用。最终达到 -Xss 的值时,会抛出StackOverFlowError。其实这里就是线程的栈内存溢出,背后的概念与 OOME 是一样的,只是jvm设计者取的名字不一样而已。
小生不才,可能还有很多技术点没有想到。希望大家多多批评指教,共同学习进步!