Jvm虚拟机学习

一、组成及其作用

1、类加载器

虚拟机把描述类的数据从Class文件加载到内存,并且对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型;

1.1、类加载过程

  • 加载:根据查找路径导入相应的class文件;
  • 验证:文件格式验证、元数据验证、字节码验证、符号引用验证,检查加载的class文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接应用的过程,符号引用用一组符号来描述所引用的目标,在直接引用中直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作;

1.2、双亲委派模型

工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式;
优势
避免类的重复加载;

1.3、类加载器分类

  • 启动类加载器:加载Java的核心库;
  • 扩展类加载器:加载Java的扩展库;
  • 应用程序类加载器:加载Java应用的类;

2、运行时区域

2.1、 程序计数器

记录当前正在执行的虚拟机字节码的指令地址;
如果正在执行的是本地方法则为空;

2.2、Java虚拟机栈

每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法的调用直至完成的过程中,对应一个栈帧在Java虚拟机中入栈和出栈的过程;

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

2.3、本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

2.4、堆

所有对象在这里分配内存,是垃圾回收的主要区域(“GC堆”),被所有线程所共享;
从内存回收的角度,现在的垃圾收集器都是采用分代收集算法,主要是针对不同类型的对象采取不同的垃圾回收算法,可以将堆分为两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

其中新生代按照8:1:1的比例分为Eden区、from Survivor、to Survivor三个区域,

堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常,可以通过 -Xms和-Xmx这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设定初始值,第二个参数设定最大值;

2.5、方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;

在Java1.8开始,移除永久代,并且把方法区移至元空间,位于本地内存中,而不是虚拟机内存中;

2.6、运行时常量池

运行时常量池是方法区的一部分,Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。

二、垃圾收集

如何判断一个对象是否可被回收

1、引用计数法

为对象添加一个引用计数器,当对象增加一个引用计数器加1,引用失效时计数器减1,引用计数为0的对象可以被回收;
缺点:无法解决对象直接的循环引用问题;

2、可达性分析算法

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。Java虚拟机中使用该算法来判断对象是否可以回收,GCRoots一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象;
  • 本地方法栈中 JNI 中引用的对象;
  • 静态成员变量或者常量引用的对象;

3、一个对象有多个引用,如何判断它的可达性

单弱多强

引用类型

1、强引用

被强引用关联的对象不会被回收,使用new一个新对象的方式来创建强引用:

Object obj = new Object();

2、软引用

被软引用的对象只有在内存不够的情况下才会被回收,使用SoftReference类来创建软引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;

3、弱引用

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。使用WeakReference 类来创建弱引用:

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

4、虚引用

为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知;
使用PhantomReference来创建虚引用:

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj,null);
obj = null;

垃圾收集算法

1、标记-清除算法

首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象;

不足:

  • 标记和清除过程的效率都不高;
  • 标记清除之后会产生大量不连续的内存碎片,而导致在分配较大对象时因为无法得到足够的连续内存而不得不提前触发一次垃圾收集动作;

2、标记-整理算法

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存;不会产生内存碎片;

不足:

  • 需要移动大量对象,处理效率比较低;

3、复制

将可用内存分为大小相等的两块,每次只使用其中的一块,当其中一块内存用完之后,就将存活的对象复制到另外一块上,然后再把已使用的内存空间一次性清理掉,使得每次都可以对整个半区进行内存回收

在商业虚拟机中并不需要按照1:1的比例进行划分内存空间,将内存分为一个较大的Eden和两块较小的Survior空间;当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间;

HotSpot虚拟机默认Eden和Sruvivor的大小比例是8:1;

4、分代收集算法

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

  • 新生代使用:复制算法(新生代中每次垃圾收集时都有大批对象死去,只有少量对象存活)
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法(老年代对象中因为对象存活率高、没有额外的空间进行分配担保)

垃圾收集器

1、老年代回收器

CMS

CMS收集器是牺牲吞吐量为代价来获取最短停顿时间为目标的垃圾回收器,
CMS收集器是基于“标记——清除”算法,整个过程分为四个步骤:

  • 初始标记:仅仅标记一下GC Roots能直接关联到的对象;
  • 并发标记:进行GC Roots Tracing,耗时最长不需要停顿;
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;
  • 并发清理:不需要停顿;

优点:并发收集、低停顿;
缺点:

  • 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不高;
  • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收;
  • 标记-清除算法导致得到空间碎片,导致会给大对象的内存分配出现问题;

Serial old

作为Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

Parallel old

Parallel Scavenge 收集器的老年代版本。

在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

2、新生代回收器

serial

单线程收集器,只会使用一个线程进行垃圾回收工作;

优点:简单高效,没有线程交互的开销,拥有最高的单线程收集效率;

在内存不大的场景下,收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这些停顿时间是可以接受的;

Parnew

Serial 收集器的多线程版本;

Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用;

Parallel Scavenge

与 ParNew 一样是多线程收集器
达到可控制的吞吐量,吞吐量是指CPU用于运行用户程序的时间占总时间的比值;

高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算程序;

缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。

3、整堆回收器

G1

G1是一种兼顾吞吐量和停顿时间的GC实现,是JDK9以后的默认GC选项;一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。

通过引入Region的概念,将原先的一整块内存空间划分成多个小空间,使得每个小空间可以单独进行垃圾回收;

步骤分为四步:

  • 初始标记:
  • 并发标记:
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  • 筛选回收:首先对于各个Region内中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划;此阶段也可以做到和用户程序一起并发执行,但是因为只回收一部分Region,时间时用户可控制的,而且停顿用户线程将大幅度提高收集效率;

空间整合:
从整体来看是基于"标记-整理"算法实现的收集器,从局部上来看是基于复制算法实现的,这意味着运行期间不会产生内存空间碎片;

最大的特点是引入了分区的思路,弱化了分化的概念;

每个分区被标记了E、S、O和H,H表示这些Region中存储的是巨型对象,新建对象大小超过Region大小一半时,直接在歆的一个或者多个连续分区中分配,并标记为H;

三、内存分配和回收策略

内存分配和回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,因为新生代对象存活时间很短,Minor GC就会频繁执行,执行的速度一般也会比较快;
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此Full GC很少执行,执行速度会比Minor GC慢很多;

内存分配策略

1、对象优先在Eden分配

大多数情况下,对象在新生代Eden上分配,当Eden空间不够时,发起Minor GC

2、大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组;

经常出现大对象会提前出发垃圾收集来获取足够的连续空间分配给大对象;

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在Eden和Survivor之间的大量内存复制;

3、长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中;

-XX:MaxTenuringThreshold 用来定义年龄的阈值;

4、动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5、空间分配担保

在发起Minor GC之前,虚拟机先检查老年代中最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么Minor GC可以确认是安全的;

Full GC的触发条件

对于MinorGC,其触发条件十分简单,当Eden空间满时,就将触发一次Minor GC,而Full GC相对复杂,条件如下:

1、调用 System.gc()

只是建议虚拟机执行 FullGC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2、老年代空间不足

老年代空间不租的常见问题是大对象直接进入老年代、长期存活的对象进入老年代等;

应当避免创建过大的对象和数组,除此之外还可以通过-Xmn虚拟机参数来调大新生代的大小,让对象尽量在新生代中被回收掉,不进入老年代;还可以通过-XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代中多存活一段时间;

3、空间分配担保失败

使用复制算法的 MinorGC需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC;

4、Concurrent Mode Failure

执行CMS GC的过程中同时有对象要放入老年代中,而此时的老年代空间不租(可能是GC过程中浮动垃圾过多导致暂时性地空间不足),便会报Concurrent Mode Failure错误,并且触发Full GC;

内存泄漏

  • 程序动态分配了内存,但是在程序结束时,没有进行及时释放,导致那部分内存不可用;
  • 被分配地对象可达但是已经没有作用,循环创建对象,各种连接没有及时释放;

可以通过一些性能检测工具,如JProfiler等工具查找内存泄漏;

内存溢出

  • 虚拟机和本地方法栈溢出
  • 堆溢出
  • 方法区溢出

内存溢出和内存泄漏的区别

  • 内存泄漏导致内存溢出的原因之一;内存泄漏积累起来将导致内存溢出;
  • 内存泄漏可以通过完善代码来避免;内存溢出可以通过调整配置来减少发生频率,但无法彻底避免;

如何避免内存泄漏和溢出?

  • 尽早释放无用对象的引用;
  • 采用临时变量的时候,让引用变量在退出活动域之后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄漏;
  • 程序进行字符串处理时,应该避免使用string,而应使用StringBuffer,因为每一个String对象都会独立占用内存一块区域;

相关面试题

1、如何减少GC次数?

  • 尽量少用静态变量;
  • 对象不用时最好显式设置为null;
  • 增大堆的最大值设置;
  • 尽量使用stringBuffer而不是String,减少不必要的中间对象
  • 经常使用的图片可以使用软引用类型;

2、对象在内存中的初始化过程

https://blog.csdn.net/WantFlyDaCheng/article/details/81808064

以Student s = new Student()为例:

  1. 首先查看类的符号引用,看是否在常量池中,不在的话进行类加载的过程;
  2. 在栈内存为s变量申请一个空间;
  3. 在堆内存中为Student对象申请空间;
  4. 对类中的成员变量进行默认初始化
  5. 对类中的成员变量进行显示初始化;
  6. 有构造代码块就先执行,没有就省略;
  7. 执行构造方法,通过构造方法来对对象数据进行初始化;
  8. 堆内存中的数据初始化完毕之后,把内存值复制给s变量;

3、一般Java堆是如何实现的?

4、对象的强、软、弱和虚引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章