原文:https://github.com/Homiss/Java-interview-questions/blob/master/JVM/JVM%E9%9D%A2%E8%AF%95%E9%A2%98.mdjava
Java虚拟机是一个能够执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。
Java被设计成容许应用程序能够运行在任意的平台,而不须要程序员为每个平台单独重写或者是从新编译。
Java虚拟机让这个变为可能,由于它知道底层硬件平台的指令长度和其余特性。android
不是java语言跨平台,而是Java虚拟机跨平台git
方法区和堆是全部线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。程序员
Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。github
方法区(Method Area),方法区(Method Area)与Java堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。面试
程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的做用能够看作是当前线程所执行的字节码的行号指示器。算法
JVM栈(JVM Stacks),与程序计数器同样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每一个方法被执行的时候都会同时建立一个栈帧(Stack Frame)用于存储局部变量表、操做栈、动态连接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。编程
本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的做用是很是类似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。数组
一般咱们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;
而经过new关键字和构造器建立的对象则放在堆空间,堆是垃圾收集器管理的主要区域,因为如今的垃圾收集器都采用分代收集算法,
因此堆空间还能够细分为新生代和老生代,再具体一点能够分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;
方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;
程序中的字面量(literal)如直接书写的100
、"hello"
和常量都是放在常量池中,常量池是方法区的一部分。
栈空间操做起来最快可是栈很小,一般大量的对象都是放在堆空间,栈和堆的大小均可以经过JVM的启动参数来进行调整,
栈空间用光了会引起StackOverflowError
,而堆和常量池空间不足则会引起OutOfMemoryError
。浏览器
String str = new String("hello");
上面的语句中变量str放在栈上,用new建立出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。
补充1:较新版本的Java(从Java 6的某个更新开始)中,因为JIT编译器的发展和"逃逸分析"技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象必定分配在堆上这件事情已经变得不那么绝对了。
补充2: 运行时常量池至关于Class文件常量池具备动态性,Java语言并不要求常量必定只有编译期间才能产生,运行期间也能够将新的常量放入池中,String类的intern()方法就是这样的。
看看下面代码的执行结果是什么而且比较一下Java 7之前和之后的运行结果是否一致。
String s1 = new StringBuilder("go") .append("od").toString(); System.out.println(s1.intern() == s1); String s2 = new StringBuilder("ja") .append("va").toString(); System.out.println(s2.intern() == s2);
对象优先分配在Eden区,若是Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指须要大量连续内存空间的对象)。这样作的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每一个对象定义了一个年龄计数器,若是对象通过了1次Minor GC那么对象会进入Survivor区,以后每通过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。若是Survivor区中相同年龄的全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象能够直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,若是这个值大于老年区的剩余值大小则进行一次Full GC,若是小于检查HandlePromotionFailure设置,若是true则只进行Monitor GC,若是false则进行Full GC。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。
[image_1bl9t477ei1r1e0k193mpu61q2im.png-24kB][2]
启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK表明JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,而且能被虚拟机识别的类库
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的全部类库(如javax.*开头的类),开发者能够直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器
答:JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
因为Java的跨平台性,通过编译的Java源程序并非一个可执行程序,而是一个或多个类文件。当Java程序须要使用某个类时,JVM会确保这个类已经被加载、链接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,一般是建立一个字节数组读入.class文件,而后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,因此此时的类还不可用。当类被加载后就进入链接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)若是类存在直接的父类而且这个类尚未被初始化,那么就先初始化父类;2)若是类中存在初始化语句,就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采起了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其余的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
因为Java的跨平台性,通过编译的Java源程序并非一个可执行程序,而是一个或多个类文件。当Java程序须要使用某个类时,JVM会确保这个类已经被加载、链接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,一般是建立一个字节数组读入.class文件,而后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,因此此时的类还不可用。当类被加载后就进入链接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。
从Java 2(JDK 1.2)开始,类加载过程采起了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其余的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
1.JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。而后加载这个类(类加载过程在后边讲)
2.为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终经常使用的办法“本地线程缓冲分配(TLAB)”
3.将除对象头外的对象内存空间初始化为0
4.对对象头进行必要设置
类的生命周期包括这几个部分,加载、链接、初始化、使用和卸载,其中前三部是类的加载的过程,以下图;
[image_1bl9t1geg15lj58p41kd151k699.png-41.6kB][3]
Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(通常占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象表明哪一个类)。若是是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和本身定义的)
对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
句柄池、直接指针。
判断对象是否存活通常有两种方式:
垃圾回收不会发生在永久代,若是永久代满了或者是超过了临界值,会触发彻底垃圾回收(Full GC)。若是你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为何正确的永久代大小对避免Full GC是很是重要的缘由。请参考下Java8:从永久代到元数据区
(注:Java8中已经移除了永久代,新加了一个叫作元数据区的native内存区)
答:GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会致使程序或系统的不稳定甚至崩溃,Java提供的GC功能能够自动监测对象是否超过做用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操做方法。Java程序员不用担忧内存管理,由于垃圾收集器会自动进行管理。要请求垃圾收集,能够调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM能够屏蔽掉显示的垃圾回收调用。
垃圾回收能够有效的防止内存泄露,有效的使用可使用的内存。垃圾回收器一般是做为一个单独的低优先级的线程运行,不可预知的状况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或全部对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,由于服务器端的编程须要有效的防止内存泄露问题,然而时过境迁,现在Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户一般以为iOS的系统比Android系统有更好的用户体验,其中一个深层次的缘由就在于android系统中垃圾回收的不可预知性。
补充:垃圾回收机制有不少种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要建立的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,可是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不一样的区域,在垃圾收集过程当中,可能会将对象移动到不一样区域:
-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:设置幸存区的目标使用率
1.该对象没有与GC Roots相连
2.该对象没有重写finalize()方法或finalize()已经被执行过则直接回收(第一次标记)、不然将对象加入到F-Queue队列中(优先级很低的队列)在这里finalize()方法被执行,以后进行第二次标记,若是对象仍然应该被GC则GC,不然移除队列。
(在finalize方法中,对象极可能和其余 GC Roots中的某一个对象创建了关联,finalize方法只会被调用一次,且不推荐使用finalize方法)
方法区回收价值很低,主要回收废弃的常量和无用的类。
如何判断无用的类:
1.该类全部实例都被回收(Java堆中没有该类的对象)
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方利用反射访问该类
GC最基础的算法有三种:
标记 -清除算法、复制算法、标记-压缩算法,咱们经常使用的垃圾回收器通常都采用分代收集算法。
标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字同样,算法分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收掉全部被标记的对象。
复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。
标记-压缩算法,标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。
摘录GC日志一部分(前部分为年轻代gc回收;后部分为full gc回收):
2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
经过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收先后年轻代的内存变化;ParOldGen表示gc回收先后老年代的内存变化;PSPermGen表示gc回收先后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,所以通常尽可能减小full gc的次数
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
经常使用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。
新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC
-Xmx:堆内存最大限制。
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比