Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,再通过JVM中的解释器,编译成特定机器上的机器码。换一种说法:
JVM的工作过程是将Java类文件加载在内存中,并解释执行字节码,通过JIT编译器把解释执行的字节码转换为本地机器码,同时定期执行垃圾回收以释放内存空间。
如果按照这种方式回答,估计会被问到垃圾回收器、JIT、类加载机制
内存区域:
JVM的内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区域【Java堆、方法区】、直接内存。
生命周期:
线程私有数据区域生命周期与线程相同,依赖用户线程的启动而创建,结束而销毁。
线程共享区域随着虚拟机的启动而创建,关闭而销毁。
直接内存不是JVM运行时数据区的一部分,生命周期不受JVM管理,而是由操作系统管理的,所以生命周期视操作系统而定。
需要注意的是:尽管线程私有区域的生命周期与线程相同,但是虚拟机栈中的栈帧对象可能会在方法执行完后被垃圾回收器回收,而且虚拟机栈的大小可以通过JVM启动参数设置,所以虚拟机栈的生命周期也并非完全由线程决定。
存放内容:
程序计数器:用来存储当前线程正在执行的字节码指令的地址,也可以用来记录线程执行Java方法、Native方法的当前行号,以及Java虚拟机正在执行的指令地址。
是一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每个线程多要有一个独立的程序计数器,这类内存也被称为“线程私有”的内存。
正在执行Java方法的话,程序计数器记录的是虚拟机字节码执行的地址(当前指令的地址),如果是Native方法,则为空。
这个内存区域是JVM中唯一一个没有规定任何OOM(内存超限)情况的区域。
Native方法:是Java语言中使用非Java语言(例如C、C++)实现的方法,主要作用是扩展Java语言的功能,因为Java语言并不能处理所有的任务。Native方法可以直接访问操作系统底层的资源,因此比纯Java方法更加高效。
虚拟机栈:用来存储Java方法的局部变量、操作数栈、动态链接、方法出口等信息。
每个方法在执行的同时都会创建一个栈帧,用来存储Java方法的局部变量、操作数栈、动态链接、方法出口等信息,每一个方法从调用到执行完成就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧是用来存放数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。栈帧随着方法调用而创建,随着方法结束而销毁(无论方法是正常完成还是异常完成都算方法结束)。
本地方法栈:用来存储Native方法的局部变量、操作数栈等信息。和虚拟机栈类似,但是它们分别作用于Java方法和Native方法。
Java堆:是JVM最大的一块内存区域,也是垃圾回收机制的主要工作区域,用来存储Java对象实例。
现在VM采用的基本是分代收集算法,因此Java堆从GC的角度还可分为新时代和老年代。
方法区:用来存储已加载的类信息、常量、静态变量、编译器编译后的代码等数据。
方法区,也就是我们常说的永久代。运行时常量池也是方法区的一部分。
运行时常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
直接内存:不是Java虚拟机的一部分,通常用于当做缓存存放Java数据。
一般占了堆的1/3空间。新生代又分为了Eden区(8/10),ServivorFrom(1/10)、ServivorTo(1/10)三个区。
Eden区:Java新对象的出生地,当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。需要注意的是,如果新创建的对象占用内存很大,则直接分配到老年代。
ServivorFrom:上一次GC的幸存者,作为这次GC的被扫描者。
ServivorTo:保留了GC过程中的幸存者。
MinorGC采用复制算法。
大致可以用复制->清空->互换三个步骤来说。
第一,Eden、ServivorFrom区域中存活的对象复制到ServivorTo区域(如果有对象的年龄达到了年老的标准,则复制到老年代区) ,同时把这些对象的年龄+1(如果ServivorTo内存不够了就放在老年区)
第二,清空Eden、ServivorFrom。
第三,ServivorTo和ServivorFrom互换,让原ServivorTo成为下一次GC时的ServivorFrom区。
一般来说有两种方法,分别是引用计数法和可达性分析。
引用计数法:
在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然的一个办法就是通过引用计数来判断一个对象是否可以被回收。即一个对象如果没有任何与之关联的引用,则一个对象如果没有任何与之关联的引用,即他们的引用计数都为0,则说明对象不太可能再被用到,那么对象就是可回收对象。
引用计数法的缺点:
1.循环引用问题:如果两个或多个对象的引用计数都为1,但它们之间有相互引用情况,那么久可能导致内存泄漏问题。
2.计数器更新问题:引用计数法需要在每个对象中维护计数器,每次引用关系的建立和断开都需要更新计数器,会带来额外开销。
3.计数器溢出问题:引用计数法使用的是有限的计数器,如果计数器溢出,就可能导致计算不准确,导致内存泄漏或错误的回收。
4.分代垃圾回收问题:引用计数法是按照数量回收的,不能很好的判断对象的存活时间。
可达性分析:
为了解决引用计数法的循环引用问题,Java采用了可达性分析。通过“GC roots”对象作为起点搜索,如果在“GC roots”和一个对象之间没有可达路径,则该对象是不可达的。需要注意的是,不可达对象不等价于可回收对象,不可达对象要变为可回收对象至少要经过两轮标记过程。
1.虚拟机栈中引用的对象
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象。JVM中字符串常量池和类常量池中可能包含了对象的引用
4.本地方法栈中JNI引用的对象。
5.虚拟机内部的引用。
1.标记-清除算法
2.复制算法
3.标记-整理算法
4.分代收集算法
5.并发标记-清除算法
6.G1算法