水手辛巴德

V1

2022/10/03阅读:25主题:橙心

程序员八股文之JVM篇

微信公众号:辛巴德笔记
关注我,一起修炼编程内功,一起加油!

聊聊JVM内存结构

JVM内存结构
JVM内存结构

JVM内存结构一共分为5大区域,堆、本地方法栈、虚拟机栈(Java栈)、程序计数器、方法区。

线程共享数据区域

堆(heap)

  • 线程共享区域
  • 用于存放对象实例
  • 垃圾回收主要在此区域
  • 新生代(Eden空间、from survivor、to survivor)、老年代

方法区

  • 线程共享区
  • 类结构信息、方法代码、常量、静态变量等数据在此区域

线程隔离数据区域

虚拟机栈(Java栈)

  • 线程私有
  • 由栈桢组成,一个方法的生命周期就贯彻了一个栈桢从入栈到出栈的全部过程
  • 生命周期与线程保持一致

本地方法栈

  • 线程私有
  • 其实就是Java底层由C语言编写的native方法。代表本地方法,本地方法栈即为其服务

程序计数器

  • 线程私有
  • 用来控制当前线程所执行的字节码行号指示器,由它实现了代码的流程控制>如:代码的顺序实行、跳转、循环、异常处理
  • 多线程情况下,用来记录当前线程执行的位置,当线程来回切换时能够知道上次执行的位置

如何判断对象存活状态?

引用计数法

概念

对于任何一个对象,只要有任何一个对象引用了该对象,则该对象的引用计数器+1,当该对象引用失效时,引用计数器就-1。

若当前对象的引用计数器数值=0,则表示该对象不在被使用,可以回收。

总结

  • 存在对象循环引用问题
  • Obj1对象引用了Obj2,Obj2对象也引用了Obj1,导致他们的引用计数器始终不为0,导致JVM无法垃圾回收

可达性分析(JVM默认使用)

概念

以GC Roots作为起点,根据引用关系向下搜素,搜索过程称为引用链若某个对象到GC Roots之间没有任何引用链接,表示该对象未被使用,可以垃圾回收了!

如下图中右侧三个对象Object6、7、8之间虽有引用,但是没有与GC Roots相连,因此可以被垃圾回收。 GC Roots

GC Roots对象

  • 虚拟机栈中引用的对象,如:各个线程被调用的方法堆栈中用到的参数、局部变量、临时变量等
  • 类静态属性引用的对象,如:静态变量(JDK7在方法区,JDK8在堆内)
  • 常量引用对象,如字符串(StringTable)常量池的引用
  • 被同步锁(Synchronized)持有的对象
  • 本地方法栈中native方法引用的对象

垃圾收集算法?

复制算法

  • 优点是,可保证内存连续性,不会出现内存碎片问题
  • 缺点是,浪费空间,需要占用两倍空间
  • 在新生代中,一次通常可回收70%~90%的空间,回收性价比高,因此一般商业虚拟机都是采用复制算法回收新生代。

标记清除算法

标记出需要回收的垃圾,然后清理掉 标记清理

  • 需要等待完全标记完才开始进行回收,效率不高
  • 在进行GC时,需要停止整个应用程序,用户体验差
  • 内存不连续,产生内存碎片
  • 对于清除而言,并不是真正的清除,而是把清除的对象地址保存在空闲的地址列表里,需要维护空闲列表

标记压缩(/整理)算法

从根节点开始标记所有被引用的对象,将所有的存活对象压缩到内存一端,按顺序排放,之后清理界外所有的空间

  • 优点,相对标记-清除算法,内存消耗占用减少了;相对复制算法,消除了内存减半的代价
  • 缺点,效率低下,移动对象时,若对象被其他对象引用,需要调整引用的地址

分代收集算法

  • 一般新生代使用复制算法。在新生代中,每次垃圾收集都有大批对象回收,使用复制算法比较合适
  • 老年代采用标记-清除或者标记-整理算法。老年代对象存活率高

垃圾回收器?

Serial(串行回收)

一、Serial GC

介绍

在进行垃圾回收时必须暂停其他所有工作线程(Stop The World),直至回收结束。

总结

  • 新生代串行垃圾收集器
  • 复制算法
  • 简单高效、内存消耗小;响应速度优先
  • 适用于单CPU环境下的CLient模式

二、Serial Old GC

  • 老年代串行垃圾收集器(被废弃)
  • 标记-压缩算法
  • 响应速度优先
  • 适用于单CPU环境下的CLient模式

Parallel(并行回收)

三、ParNew GC

介绍

其实就是“一、Serial GC”的多线程版本

总结

  • 新生代并行垃圾收集器
  • 复制算法
  • 响应速度优先
  • 多CPU环境Server模式下与CMS配合使用

四、Parallel Old GC

介绍 Parallel Old其实就是Parallel ScanVenge的老年代版本;其设计思路也是以吞吐量优先

总结

  • 老年代并行垃圾收集器
  • 标记-压缩算法
  • 吞吐量优先
  • 适用于后台运算、不需要太多交互的场景

五、Parallel Scavenge GC

介绍

与ParNew类似,都属于采用复制算法回收年轻代的并行收集器;与ParNew不同之处在于,Parallel的目标是达到一个可控的吞吐量。

Parallel提供了两个参数用以精确控制吞吐量,分别是用以控制最大GC停顿时间-XX:MaxGCPauseMillis直接控制吞吐量的参数-XX:GCTimeRatio

总结

  • 新生代并行垃圾收集器
  • 复制算法
  • 吞吐量优先
  • 适用于后台运算、不需要太多交互的场景

CMS(并发回收)

六、CMS(Concurrent Mark Sweep)

介绍

支持用户线程和GC线程一起工作,具有跨时代意义

cms
cms

初始标记: 暂停所有用户线程(STW),记录直接与GC Roots相连接的对象

并发标记: 从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但无需停顿用户线程

重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记,此阶段会暂停所有用户线程

并发清除: 清除标记对象,该阶段也是可与用户线程同时并发进行的

总结

  • 老年代并发标记回收器
  • 标记-清除算法
  • 响应速度优先
  • 适用于互联网、B/S业务

七、G1

介绍

将堆内存分割成不同区域、然后并发的进行垃圾回收

G1
G1

总结

  • 新生代、老年代都可以用的并发回收器
  • 标记-压缩算法、复制算法
  • 响应速度优先
  • 面向服务端应用

Minor GC和Full GC区别?

minor GC

  • 回收新生代,新生代对象存活时间很短,所以minor GC执行很频繁,执行速度也很快
  • Eden区满了,则触发minor GC;Survior区满了不会触发minor GC
  • 会引发STW(Stop The World)

Full GC

  • 老年代空间不足时,回收老年代和新生代,老年代对象存活时间长,因此Full GC很少执行
  • 方法区空间不足时
  • 手动调用System.gc(),调用后并不是立即生效的!
  • minor GC后进入老年代的平均大小>老年代的可用空间
  • Eden区向survivor区复制存活对象时,survivor区存放不下后,将该对象放入老年代,且老年代可用空间也存放不下该对象

major GC

  • 永久代(元空间)满了,则触发major GC

Java四种引用类型?

强引用

  • 可以直接访问的对象,如:Object obj=new Object();
  • 强引用指向的对象在任何时候都不会被回收
  • JVM宁愿抛出OOM异常,也不会回收强引用的对象

弱引用

  • 只要是弱引用,都会被垃圾回收器回收掉
  • WeakReference<String> weakRef = new WeakReference<String>(str);

软引用

  • 内存不足时,再回收软引用的可达对象;
  • SoftReference<String> softRef = new SoftReference<String>(str);

虚引用

  • 主要用来追踪对象的回收情况

JVM调优参数

  • -Xms1g:初始化堆大小为 1g;
  • -Xmx1g:堆最大内存为 1g;
  • -XX:NewRatio=4:设置新生代和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置 Eden 和 Survivor 比例为 8:2;
  • -XX:+PrintGC:打印 GC 信息;
  • -XX:+PrintGCDetails:打印 GC 详细信息;
  • –XX:+UseParNewGC:指定使用 ParNew+ Serial Old 垃圾回收器;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器。

JVM调优工具

jps

  • 列出当前所有的Java进程号
  • jps -lvm
    • -m 输出main方法的参数
    • -l 输出完全的包名和应用主类名
    • -v 输出JVM 参数

jstack

  • 查看某个Java进程的堆栈信息
  • 使用参数-l可以打印额外的锁信息
  • 发生死锁时,可以用 jstack -l pid观察锁持有情况;例:jstack -l 8741 | more

jstat

  • 用于查看虚拟及各种运行状态信息,比如类装载、内存、垃圾收集器等运行数据
  • 例如,jstat -gcutil 8741 可以查看垃圾回收的统计信息

jmap

  • 查看堆内存快照
  • 例如,jmap -heap 8741

什么情况会发生栈溢出?

  • 递归没终止条件就会发生StackOverFlowError异常,也就是当线程请求的栈深度超过了虚拟机允许的最大深度
  • 线程启动过多时,也就是新建线程时没有足够的内存去创建对应的虚拟机栈,虚拟机会抛出OutOfMemoryError异常

内存溢出和内存泄漏?

内存溢出(out of memory)

  • 代码中创建了大量的大对象,导致垃圾回收器来不及回收,分配堆内存被占满
  • Java虚拟机的堆内存设置不够

内存泄漏(memory leak)

  • 对象是可达的,但对象不在会被程序使用到了,所以导致GC无法回收的情况
  • 例如:数据库链接、网络连接、IO连接必须手动close(),否则不能回收!
  • 内存泄漏的堆积最终会导致内存溢出

堆栈的区别?

  • 堆是线程共享的,栈是线程私有的
  • 堆存放的是对象的实例和数组,栈存放的是局部变量、操作数栈和返回结果等
  • 堆的物理地址分配时不连续的,性能较慢;栈则与之相反

类加载过程与双亲委派机制

类加载过程

类加载过程就是类的class文件中的二进制数据读到内存中,将其放在运行时数据区的方法内,然后在堆中创建一个此类对象,通过这个对象可以访问到方法区内对应的类信息

加载过程
加载过程

双亲委派机制

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派是为了安全而设计的。假如我们自定义了一个java.lang.Integer类如下,当使用它时,因为双亲委派,会先使用BootStrapClassLoader来进行加载,这样加载的便是jdk的Integer类,而不是自定义的这个,避免因为加载自定义核心类而造成JVM运行错误。

说的简单点其实就是为了保护JDK的安全性,如果你自己写了一个System这个类,如果没有这种加载机制,那么你自己写的这个类将被加载,这样对JDK的入侵是很大的,那么现在有这种机制,BootStrapClassLoader会去rt包下找是否有System这个类,如果有就会直接加载JDK自己的这个类,对于新定义的这个类则不会做处理

对象创建过程

分类:

后端

标签:

后端

作者介绍

水手辛巴德
V1