JVM之内存管理与垃圾回收

https://www.yuque.com/u21195183/jvm/qpoa81

说说JVM内存模型

根据《JAVA虚拟机规范》将java内存划分为程序计数器、虚拟机栈、本地方法栈、堆和方法区。

1. 程序计数器
是用于存储字节码行号的,当线程获取cpu执行权时,根据该值可以知道上一次的执行位置。这个内存没有OOM也不会进行GC。
2. 虚拟机栈与本地方法栈
由于hotspot虚拟机的实现没有区分虚拟机栈与本地方法栈,所以这里就一起说了,虚拟机栈是线程的执行模型,每一个栈帧代表一个方法。一个栈帧从入栈到出栈的过程就是一个方法执行开始到结束的过程。栈帧又能细分为局部变量表、操作数栈、动态链接和返回地址等。
本地方法栈就是用于执行本地方法的。和程序计数器一样,也是线程独有的,也不会进行GC,但是会OOM
3. 堆
堆是jvm管理的最大的一块内存,主要用于存放对象,这里是GC主要清理的区域,是线程共享的。
4. 方法区
方法区用于存放类型数据、常量、静态变量、热点代码等。在低版本中也叫永久代,jdk7开始去永久化,将字符串常量、静态变量移到堆中。jdk8彻底去除永久代,改用本地内存存储类型数据,代码缓存等其他值,称为元空间。

那栈帧的各个部分存储了什么,有什么作用呢?

  1. 局部变量表
    存放了编译期便可知的基本数据类型数据、方法引用和方法返回地址,也就是每个方法的参数和内部定义的局部变量。使用jclasslib可以看到每个方法的LocalVariableTable,就是局部变量表。
  2. 操作数栈
    用于存放计算时的数据,使用jclasslib可以看的StackMapTable就是操作数栈
  3. 动态连接
    用于存放一个指向运行时常量池中的所属方法的引用。
  4. 方法返回地址
    一个方法有两种退出方式。正常返回和异常返回。无论那种返回,方法退出之后,都应该回到最初被调用的位置,程序才能继续执行。

为什么要将字符串常量池移走呢?

方法区很少进行垃圾收集,只有在FULL GC时才进行,而字符串又是高频操作,这就容易使得方法区溢出。

那内存溢出时,你怎么处理?如何排查

需要使用-XX:+HeapDumpBeforeFullGC或者-XX:HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath=. -XX:+PrintGCDateStamps
-Xloggc:gc.log尽量多的记录下当时的堆快照和gc日志。然后使用日志分析工具比如JProfiler、MAT、Visual VM、JMC、Arthas等查找分析。确认导致的OOM的对象是否是必要的,确认是内存泄漏还是内存溢出。如果是内存溢出那只能加内存,但如果是内存泄漏,则需要进一步分析泄漏对象到GC Roots的引用链,找出泄漏对象是通过怎样的引用路径、与哪些GC Roots有关,才导致无法回收,在去定位到代码中的位置,进一步分析代码,找出原因。

对象的创建过程及如何分配

  1. 当jvm遇到字节码指令new时,首先会根据该指令的参数是否在常量池找到一个类的符号引用,并且判断这个类是否已完成类加载过程,如何没有则需要进行类加载过程,然后才能为其分配内存空间。
  2. 分配完成后,需要为类变量(static修饰的变量)赋初始零值,也就是clinit方法,由虚拟机自动收集需要赋值的变量生成该方法。
  3. 然后进行对象头的设置,例如这个对象是哪个类的实例、对象的哈希码)、对象的GC分代年龄等信息
  4. 当上面完成后,才开始执行构造函数即init方法。

分配内存有两种方式

指针碰撞
如果内存是整齐的,已使用的在一边,未使用的在另一边,中间是一个指针分隔,则只需要将指针往空闲区移动与对象相等大小的位置即可。

空闲列表
如果已用内存和未使用的内存混在一起,则就需要使用空闲列表记录哪些内存可用。

分配内存的安全问题处理

实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性

另外一种是把内存分配的动作按照线程划分在不同的空间之中进 行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。通过-XX:+/-UseTLAB参数来设定。默认是开启的,可以通过 jinfo -flag UseTLAB [ID]来查看

对象的存储结构是怎样的

对象由三部分构成。
对象头:由两部分构成,一是用于存储对象自身的信息,如哈希码、GC分代年龄、线程持有的锁、锁状态标志、偏向线程ID等。另外一部分是类型指针,即对象指向它的类型元数据的指针
实例对象:代码中定义的字段等信息
对齐填充。

对象的访问方式

直接指针

优点
访问速度快
缺点
垃圾收集时,如何需要移动对象,则需要修改栈中reference的值

句柄

优点
不需要修改栈中的reference的值

缺点
比直接指针慢

举几个你了解的关于内存的参数

-Xms 设置初始内存大小
-Xmx 设置最大内存大小
-Xss 设置线程栈的大小
-Xloggc: 设置gc日志文件路径

几种引用的区别?WeakHashMap有什么作用

  1. 强引用 任何情况下都不会被回收
  2. 软引用 在oom之前,会进行回收
  3. 弱引用 gc发生时就会被回收
  4. 虚引用 被虚引用的对象时无法获取到的。作用是当对象被回收时,会收到一个系统通知。

WeakHashMap,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值,所以比较适合做缓存。tomcat就使用了这个

HostSpot虚拟机如何进行内存管理?

在jdk9之前,jvm将内存划分为新生代、老年代和永久代。新生代又进一步划分成eden区和s0、s1区。新生代大多数对象朝生夕灭,采用复制算法,老年代采用标记清除算法或者标记整理算法。这和具体的垃圾收集器相关。jdk8默认的是parallel GC 吞吐量优先
jdk9开始使用区域化分代收集,有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。jdk9开始,默认G1 GC

查看jvm使用的垃圾收集器:-XX:+PrintCommandLineFlags
指定垃圾收集器命令:-XX: +UseG1GC

对象什么时候进入老年代呢?

  1. 在经过多次垃圾收集,年龄到达阈值时(默认15)
  2. 大对象直接进入老年代
  3. 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

什么是可达性分析算法?哪些对象可以作为GC Roots?

可达性分析算法是从一组GC Roots出发,根据引用关系向下遍历,直至最后,没有在引用链上的节点就属于不可达对象,则可以进行回收。

在Java中,固定可作为GC Roots的对象包括以下几种:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized关键字)持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  8. 根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入

总结一句话:其他区域指向堆的引用就是根节点。

如何处理引用计数器的缺点?

引用计数器无法处理相互引用得情况。
如何解决循环引用?
1 手动处理,在代码中手动释放
2 若 A 强引用了 B,那 B 引用 A 时就需使用弱引用,当判断是否为无用对象时仅考虑强引用计数是否为 0,不关心弱引用计数的数量,但这会引发野指针问题:当 B 要通过弱指针访问 A 时,A可能已经被销毁了,那指向 A 的这个弱指针就变成野指针了。在这种情况下,就表示 A确实已经不存在了,需要进行重新创建等其他操作

有哪些垃圾收集算法?

  1. 标记清除算法

    分为两个过程:标记阶段,从根节点出发,标记所有可达对象;清除阶段,堆内存从头到尾依次遍历,清除所有未标记对象。

    优点:实现简单。

    缺点:1 随着存活对象的增加,效率会逐渐下降;2 有内存碎片化问题;3 需要停止用户线程

  2. 复制算法

    针对标记清除算法有碎片化问题,提出改进。首先将内存分为两个区域,每次只是用其中一块,当内存已满时,就将该区域所有存活的对象移动到另一块内存中。

    优点:1 没有碎片化问题 ;2 当只有少量对象存活时,收集效率高

    缺点:1 随着存活对象的增加,效率会逐渐下降;2 会浪费一半的内存;3 需要停止用户线程

    改进: 像Hotspot中,将新生代内存划分成eden区和S0、S1,默认8:1:1,每次可使用空间为eden+一个s区,这样就可将使用率提升至90%。且新生代对象大多都是朝生夕灭的,回收效率高。

    改进后的缺点:需要老年代做空间担保。

  3. 标记整理算法

    与标记清除算法相识,第一个阶段也是标记存活对象,第二个阶段是将存活对象移动到内存的另一端顺序排放,之后清理掉边界外的所有空间。 适用于像老年代这样,无法再做空间担保的.

    优点:1 没有碎片化问题;2 不需要空间担保

    缺点:1 随着存活对象的增加,效率会逐渐下降;3 需要停止用户线程

  4. 分代收集算法

    就是结合上面三种算法的优缺点,采用内存分区,不同区域使用最适合的算法。

  5. 增量收集算法

    如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

    优点:
    减少系统的停顿时间,增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

    缺点:
    因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

  6. 分区算法

    G1开始采用的新的内存划分方式,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

    优点:每一个小区间都独立使用,独立回收。不再是整堆收集,所以收集时间更短

    缺点:1 每个区域都需要记忆集记录其他块的哪些卡表有本区域的引用,是的占用内存大;2 如果停顿时间设置过小,则每次收集的块将减少,可能赶不上分配速度,导致full gc。

什么是安全点呢?

就是程序在这一刻引用不会发生改变,这里做垃圾收集是安全的。
“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转 等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。

记忆集和卡表是什么?

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。

有哪些垃圾收集器?

单线程的收集器:serial和serial old 并行的收集器:parnew 、parallel parallel Old 并发收集器:CMS、G1、Shenandoah、ZGC

CMS的收集过程知道吗?

CMS的收集分为四个阶段

  1. 初始标记:仅只是标记一下GC Roots能直接关联到的对象,速度很快,耗时很短。
  2. 并发标记:从根节点向下递归遍历对象,耗时长,但可以与用户线程一起执行。
  3. 重新标记:对并发标记中用户线程修改的对象再做更新。耗时稍长,但比并发标记短很多
  4. 并发清除:将没有标记的对象清除。,耗时长,但可以与用户线程一起执行。

优点:
1 并发收集 2 低延迟(和用户交互友好)

缺点:
1 会产生空间碎片 2 有浮动垃圾

G1的收集过程知道吗?

G1从大方向来说有四个阶段:Young GC、并发标记、Mixed GC和Full GC

  1. 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
  2. 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
    1. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
    2. 根区域扫描:G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在YoungGC之前完成。
    3. 并发标记:在整个堆中进行并发标记(和应用程序并发执行),此过程可能被YoungGC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
    4. 最终标记:由于应用程序持续进行,需要修正并发标记的结果。是STW的。G1中采用原始快照算法
    5. 独占清理:计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集
    6. 并发清理阶段:识别并清理完全空闲的区域。
  3. 混合回收
    为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收
  4. Full GC
    如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

G1回收器优化建议

年轻代大小

  1. 避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小
  2. 固定年轻代的大小会覆盖暂停时间目标

暂停时间目标不要太过严苛

  1. G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
  2. 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

如何选择垃圾收集器呢?

  1. 优先调整堆的大小让JVM自适应完成。
  2. 如果内存小于100M,使用串行收集器
  3. 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  4. 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
  5. 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
    官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

知道哪些内存分析工具?

  1. arthars
  2. eclips MAT
  3. Jproliler
  4. vitural VM

什么是浅堆与深堆

  1. 浅堆:指对象直接关联的变量的大小,与值无关(类似浅拷贝)以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。

  2. 深堆:指只由该对象所引用的对象大小(类似深拷贝)。深堆是指对象的保留集中所有的对象的浅堆大小之和

  3. 实际大小:指该对象关联的所有对象的大小

保留集:对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。

什么是内存泄漏?可能发生的情况有哪些?

当一个对象已经不再需要使用,但是依然有根节点直接或间接的引用它,导致该对象无法被回收时。就成为内存泄漏。

发生内存泄漏有以下情况:

  1. 使用单例模式
  2. 使用静态变量,如static修饰的数组、集合、map
  3. 网络连接、数据库连接、IO操作后没有close
  4. 对hashset中的对象修改,导致哈希值改变。
  5. 缓存数据没有过期策略
  6. 变量不合理的作用域,可以是局部变量的,却使用全局变量

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值