
水手辛巴德
2022/10/03阅读:32主题:橙心
程序员八股文之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对象
-
虚拟机栈中引用的对象,如:各个线程被调用的方法堆栈中用到的参数、局部变量、临时变量等 -
类静态属性引用的对象,如:静态变量(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线程一起工作,具有跨时代意义

初始标记: 暂停所有用户线程(STW),记录直接与GC Roots相连接的对象
并发标记: 从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但无需停顿用户线程
重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记,此阶段会暂停所有用户线程
并发清除: 清除标记对象,该阶段也是可与用户线程同时并发进行的
总结
-
老年代并发标记回收器 -
标记-清除算法 -
响应速度优先 -
适用于互联网、B/S业务
七、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自己的这个类,对于新定义的这个类则不会做处理
对象创建过程
作者介绍
