唐僧:龙不是你那么骑的
想要提高程序员自身的内功心法无非就是数据结构跟算法
+ 操作系统
+ 计网
+ 底层
,而所有的Java代码都是在JVM上运行的,了解了JVM好处就是:
写出更好更健壮的代码。
提高Java的性能,排除问题。面试必问
,要对知识有一定对深度。
1、简述JVM 内存模型
从宏观上来说JVM 内存区域 分为三部分线程共享区域
、线程私有区域
、直接内存区域
。
1.1、线程共享区域
堆区Heap是JVM中最大的一块内存区域,基本上所有的对象实例都是在堆上分配空间。堆区细分为年轻代和老年代,其中年轻代又分为Eden、S0、S1 三个部分,他们默认的比例是8:1:1
的大小。
方法区:
- 在 《Java虚拟机规范》中只是规定了有
方法区
这么个概念
跟它的作用
。HotSpot
在JDK8之前 搞了个永久代
把这个概念实现了。用来主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。- PermGen(永久代)中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且为PermGen分配多大的空间因为存储上述多种数据很难确定大小。因此官方在JDK8剔除移除永久代。
官方解释移除永久代:
- This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
- 即:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
元空间:
在Java中用
永久代
来存储类信息,常量,静态变量等数据不是好办法,因为这样很容易造成内存溢出。同时对永久代的性能调优也很困难,因此在JDK8中 把永久代
去除了,引入了元空间metaspace,原先的class、field等变量放入到metaspace。
总结:
元空间的本质和永久代类似,都是对JVM规范中方法区的实现
。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。
1.2、直接内存区域
直接内存:
一般使用Native函数操作C++代码来实现直接分配堆外内存,不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。这块内存不受Java堆空间大小的限制,但是受本机总内存大小限制所以也会出现OOM异常。分配空间后避免了在Java堆区跟Native堆中来回复制数据,可以有效提高读写效率,但它的创建、销毁却比普通Buffer慢。
PS: 如果使用了NIO,本地内存区域会被频繁的使用,此时 jvm内存 ≈ 方法区 + 堆 + 栈+ 直接内存
1.3、线程私有区域
程序计数器、虚拟机栈、本地方法栈跟线程的声明周期是一样的。
课堂上比如你正在看小说《诛仙》,看到1412章节时,老师喊你回答问题,这个时候你肯定要先应付老师的问题,回答完毕后继续接着看,这个时候你可以用书签也可以凭借记忆记住自己在看的位置,通过这样实现继续阅读。
落实到代码运行时候同样道理,程序计数器用于记录当前线程下虚拟机正在执行的字节码的指令地址。它具有如下特性:
- 线程私有
多线程情况下,在同一时刻所以为了让线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。
- 没有规定OutOfMemoryError
程序计数器存储的是字节码文件的行号,而这个范围是可知晓的,在一开始分配内存时就可以分配一个绝对不会溢出的内存。
- 执行Native方法时值为空
Native方法大多是通过C实现并未编译成需要执行的字节码指令,也就不需要去存储字节码文件的行号了。
方法的出入栈:调用的方法会被打包成栈桢,一个栈桢至少需要包含一个局部变量表、操作数栈、桢数据区、动态链接。
动态链接:
当栈帧内部包含一个指向运行时常量池引用前提下,类加载时候会进行符号引用到直接引用的解析跟链接替换。
局部变量表:
局部变量表是栈帧重要组中部分之一。他主要保存函数的参数以及局部的变量信息。局部变量表中的变量作用域是当前调用的函数。函数调用结束后,随着函数栈帧的销毁。局部变量表也会随之销毁,释放空间。
操作数栈:
保存着Java虚拟机执行过程中数据
方法返回地址:
方法被调用的位置,当方法退出时候实际上等同于当前栈帧出栈。
比如执行简单加减法:
public class ShowByteCode {
private String xx;
private static final int TEST = 1;
public ShowByteCode() {
}
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
}
执行javap -c *.class
:
跟虚拟机栈类似,只是为使用到的Native方法服务而已。
2、判断对象是否存活
JVM空间不够就需要Garbage Collection
了,一般共享区的都要被回收比如堆区以及方法区。在进行内存回收之前要做的事情就是判断那些对象是死的,哪些是活的。常用方法有两种 引用计数法
跟 可达性分析
。
2.1、引用计数法
思路是给 Java 对象添加一个引用计数器,每当有一个地方引用它时,计数器 +1;引用失效则 -1,当计数器不为 0 时,判断该对象存活;否则判断为死亡(计数器 = 0)。
优点:
实现简单,判断高效。
缺点:
无法解决 对象间 相互循环引用 的问题
class GcObject {
public Object instance = null;
}
public class GcDemo {
public static void main(String[] args) {
GcObject object1 = new GcObject(); // step 1
GcObject object2 = new GcObject(); // step 2
object1.instance = object2 ;//step 3
object2.instance = object1; //step 4
object1 = null; //step 5
object2 = null; // step 6
}
}
step1: GcObject实例1的引用计数+1,实例1引用数 = 1
step2: GcObject实例2的引用计数+1,实例2引用数 = 1
step3: GcObject实例2的引用计数+1,实例2引用数 = 2
step4: GcObject实例1的引用计数+1,实例1引用数 = 2
step5: GcObject实例1的引用计数-1,结果为 1
step6: GcObject实例2的引用计数-1,结果为 1
如上分析发现实例1跟实例2的引用数都不为0而又相互引用,这两个实例所占有的内存则无法释放。
2.2、可达性分析
很多主流商用语言(如Java、C#)都采用引用链法
判断对象是否存活,大致的思路就是将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索。在Java语言中,可作为 GC Roots 的对象包含以下几种:
-
第一种是虚拟机栈中的引用的对象,在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
-
第二种是我们在类中定义了全局的静态的对象,也就是使用了
static
关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。 -
第三种便是常量引用,就是使用了
static final
关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。 -
第四种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用Native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。
GC Root步骤主要包含如下三步:
当一个对象到 GC Roots 没有任何引用链相连时,则判断该对象不可达。
注意: 可达性分析仅仅只是判断对象是否可达,但还不足以判断对象是否存活 / 死亡。
筛选的条件对象 如果没有重写finalize或者调用过finalize
则将该对象加入到F-Queue中