请你谈谈对象的内存分配策略&&JVM内存问题

1 JVM内存分配策略

对象优先在 Eden 区分配

多数情况,对象都在新生代 Eden 区分配,当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC,如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;Major GC是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。

大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。

新生代使用的是复制算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制,因此对于大对象都会直接在老年代进行分配。

长期存活对象将进入老年代

虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。

2新生对象的分配机制

在这里插入图片描述

1)依据逃逸分析,判断是否能栈上分配?
如果可以:使用标量替换方式,把对象分配到VM Stack中,如果线程销毁或方法调用结束后,对象自动销毁,不需要 GC 回收器 介入。

2)判断是否大对象?
如果是:直接分配到堆上 老年代上。如果对象变为垃圾后,由老年代GC 收集器(比如 Parallel Old, CMS, G1)回收。

3)判断是否可以在 TLAB中分配?
如果是:在 TLAB堆上的Eden区分配。
否则,在 TLAB外堆上的Eden区分配。

栈上分配

将线程私有的对象打散分配在栈 VM Stack上。优点:可以在函数调用结束后自行销毁对象,不需要垃圾回收器的介入,有效避免垃圾回收带来的负面影响;栈上分配速度快,提高系统性能。局限性: 栈空间小,对于大对象无法实现栈上分配。

技术基础: 逃逸分析、标量替换

什么是逃逸分析?

Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。

逃逸分析的 JVM 参数如下:
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
显示分析结果:-XX:+PrintEscapeAnalysis

逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数。

逃逸分析优化

  1. 锁消除
    我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
    例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。

锁消除的 JVM 参数如下:

开启锁消除:-XX:+EliminateLocks
关闭锁消除:-XX:-EliminateLocks

锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。

2)标量替换
首先要明白标量和聚合量,基本类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。
标量替换的 JVM 参数如下:

开启标量替换:-XX:+EliminateAllocations
关闭标量替换:-XX:-EliminateAllocations
显示标量替换详情:-XX:+PrintEliminateAllocations

标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。

  1. 栈上分配
    当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。

TLAB 分配

TLAB,全称Thread Local Allocation Buffer, 即:线程本地分配缓存,这是一块线程专用的内存分配区域。

TLAB占用的是eden区的空间,在TLAB启用的情况下(默认开启),JVM会为每一个线程分配一块TLAB区域。

为什么需要TLAB?
这是为了加速对象的分配,由于对象一般分配在堆上,而堆是线程共用的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降。对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率。

局限性: TLAB空间一般不会太大(占用eden区),所以大对象无法进行TLAB分配,只能直接分配到堆 Heap上。

大对象

G1回收器的大对象判断:依据Region的大小(-XX:G1HeapRegionSize)来判断,如果对象大于Region50%以上,就判断为大对象Humongous Object。

3JVM内存溢出(OOM)的原因和解决方法

1堆溢出

原因:1、代码中可能存在大对象分配; 2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法:1、检查是否存在大对象的分配,最有可能的是大数组分配 ;2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题 ;3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存; 4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性。

2永久代/元空间溢出

原因:永久代是 Hotspot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:字符串常量由永久代转移到堆中.

可能原因有如下几种:1、在Java7之前,频繁的错误使用String.intern()方法 ;2、运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载 ;3、应用长时间运行,没有重启 JVM 进程:一般发生在调试时。

解决方法:
1、检查是否永久代空间或者元空间设置的过小
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类
4、放大招,重启JVM

4JVM内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

既然是自动检测回收不用对象,那Java有没有可能出现内存泄露的情况呢?

1 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
这个办法看起来挺简单的,但是如果出现 A 引用了 B,B 又引用了 A,这时候就算他们都不再使用了,但因为相互引用 计算器=1 永远无法被回收。此方法简单,无法解决对象相互循环引用的问题。

2 可达性分析算法:该算法的思路就是通过被称为引用链(GC Roots)的对象作为起点,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,最终不可用的对象会被回收。

GC Roots对象可归纳为如下几种:
1 虚拟机栈(栈帧中的局部变量表)中引用的对象;
2 方法区中类静态属性引用的对象;
3 方法区中常量引用的对象;
4 本地方法栈中JNI(即一般说的Native方法)引用的对象;

内存泄漏的案例:
1 长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。

2集合中的内存泄漏,比如 HashMap、ArrayList 等,这些对象经常会发生内存泄露。比如当它们被声明为静态对象时,它们的生命周期会跟应用程序的生命周期一样长,很容易造成内存不足。

如何检测内存泄漏?
市场上已有几种专业检查 Java 内存泄漏的工具,它们的基本工作原理大同小异:都是通过监测 Java 程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。
这些工具包括 Plumbr 、Eclipse Memory Analyzer、JProbe Profiler、JVisualVM 等。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值