JVM 自动内存管理

数据区域

  • 程序计数器:如果线程正在执行的是一个Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是本地(Native) 方法, 这个计数器值则应为空(Undefined) 。

  • Java 虚拟机栈:虚拟机栈描述的是Java方法执行的线程内存模型: 每个方法被执行的时候, Java虚拟机都会同步创建一个栈帧用于存储局部变量表、 操作数栈、 动态连接、 方法出口等信息。

  • 本地方法栈:本地方法栈是为虚拟机使用到的本地方法服务。

  • Java堆:此内存区域的唯一目的就是存放对象实例。Java堆是垃圾收集器管理的内存区域。

  • 方法区(元空间):方法区用于存放已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码。方法区是一片连续的堆空间。

随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。

JDK1.7开始了方法区的部分移除:符号引用(Symbols)移至native heap,字面量(String常量池)和静态变量移至Java堆。

Metaspace由两大部分组成:Klass Metaspace和NoKlass Metaspace。

Klass Metaspace就是用来存klass的,klass是我们熟知的class文件在jvm里的运行时数据结构。这部分默认放在Compressed Class Pointer Space中,是一块连续的内存区域。

NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,可以由多块不连续的内存组成。这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容,上面已经提到了对应场景。NoKlass Metaspace在本地内存中分配。

关于Klass

  • 运行时常量池:存放各种字面量与符号引用。
    字符串常量池仍然依靠堆,他们存储的只是堆中字符串的引用。

  • 直接内存:不是虚拟机运行时数据区的一部分。

NIO 类, 引入了一种基于通道(Channel) 与缓冲区(Buffer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了在Java堆和Native堆中来回复制数据。

本机直接内存的分配不会受到Java堆大小的限制, 但既然是内存,肯定还是会受到本机总内存大小以及处理器寻址空间的限制, 一般服务器管理员配置虚拟机参数时, 会根据实际内存去设置-Xmx等参数信息, 但经常忽略掉直接内存, 使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

  • 变量存储位置:
    在这里插入图片描述
    图是有点问题的,常量和静态变量是在堆。
    图的出处

Java 对象

创建

1、当Java虚拟机遇到一条字节码new指令时, 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。 如果没有, 那必须先执行相应的类加载过程(代加)

2、在类加载检查通过后, 接下来虚拟机将为新生对象分配内存。内存分配有两种方式:指针碰撞(Bump The Pointer)与空闲列表(Free List)。

指针碰撞:假设Java堆中内存是绝对规整的, 所有被使用过的内存都被放在一边, 空闲的内存被放在另一边, 中间放着一个指针作为分界点的指示器, 那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。

空闲列表:如果Java堆中的内存并不是规整的, 已被使用的内存和空闲的内存相互交错在一起, 那就没有办法简单地进行指针碰撞了, 虚拟机就必须维护一个列表, 记录上哪些内存块是可用的, 在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录。

因为Java堆是共享的,并发情况下会出现冲突,所以虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另外一种解决方式是把内存分配的动作按照线程划分在不同的空间之中进行, 即每个线程在Java堆中预先分配一小块内存, 称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。

3、内存分配完成之后, 虚拟机必须将分配到的内存空间(但不包括对象头) 都初始化为零值, 如果使用了TLAB的话, 这一项工作也可以提前至TLAB分配时顺便进行。

Java虚拟机还要对对象进行必要的设置, 例如这个对象是哪个类的实例、 如何才能找到类的元数据信息、 对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算) 、 对象的GC分代年龄等信息。 这些信息存放在对象的对象头(Object Header) 之中。 根据虚拟机当前运行状态的不同, 如是否启用偏向锁等, 对象头会有不同的设置方式。

4、构造函数:Class文件中的<init>()

对象在堆内存中的存储布局可以划分为三个部分: 对象头(Header) 、 实例数据(Instance Data) 和对齐填充(Padding)。

对象头包括两部分信息:Mark Word,类型指针。

Mark Word存储对象自身的运行时数据, 如哈希码 、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等,Mark Word的数据长度在32位和64位的虚拟机(未开启压缩指针) 中分别为32个比特和64个比特。
Mark Word的动态使用——并发(锁)
在这里插入图片描述

对象头的类型指针指向指向方法区的类型元数据,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

此外, 如果对象是一个Java数组, 那在对象头中还必须有一块用于记录数组长度的数据。

对象的第二部分是实例数据,存储对象的成员变量,对象继承的父类中的变量也会存储到这里。

对象的第三部分是对齐填充,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,因此, 如果对象实例数据部分没有对齐的话, 就需要通过对齐填充来补全。

访问定位

创建对象是为了使用对象,就需要在栈中生成一个reference数据去操作堆中的对象。

访问方式主要有 句柄访问 和 直接指针访问 两种。HotSpot主要使用直接指针访问。

如果使用句柄访问的话, Java堆中将可能会划分出一块内存来作为句柄池, reference中存储的就是对象的句柄地址, 而句柄中包含了对象实例数据与类型数据各自具体的地址信息。因为存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针, 而reference本身不需要被修改。

在这里插入图片描述

如果使用直接指针访问的话, Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息, reference中存储的直接就是对象地址, 如果只是访问对象本身的话, 就不需要多一次间接访问的开销,速度更快, 节省了一次指针定位的时间开销。
在这里插入图片描述

JVM参数

堆的最小值: -Xms1024m
堆的最大值: -Xmx2048m
最小最大值设置为一样可避免堆自动扩展。

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)

内存溢出时Dump堆转储快照: -XX:+HeapDumpOnOutOfMemoryError
Heap Dump存储地址: -XX:HeapDumpPath=(目录)

栈容量: -Xss180k

元空间初始值: -XX:MetaspaceSize=6m
元空间最大值: -XX:MaxMetaspaceSize=12m

在这里插入图片描述

引用

  • 强引用是最传统的“引用”的定义, 是指在程序代码之中普遍存在的引用赋值, 即类似“Objectobj=new Object()”这种引用关系。 无论任何情况下, 只要强引用关系还存在, 垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用, 但非必须的对象。 只被软引用关联着的对象, 在系统将要发生内存溢出异常前, 会把这些对象列进回收范围之中进行第二次回收, 如果这次回收还没有足够的内存,才会抛出内存溢出异常。 在JDK 1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象, 但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生为止。 当垃圾收集器开始工作, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。 在JDK 1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用也称为“幽灵引用”或者“幻影引用”, 它是最弱的一种引用关系。 一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 在JDK 1.2版之后提供了PhantomReference类来实现虚引用。
  • 引用队列。引用队列可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中。通过引用队列可以了解JVM垃圾回收情况。
// 引用队列
ReferenceQueue<String> rq = new ReferenceQueue<String>();
// 软引用
SoftReference<String> sr = new SoftReference<String>(new String("Soft"), rq);
// 弱引用
WeakReference<String> wr = new WeakReference<String>(new String("Weak"), rq);
// 幽灵引用
PhantomReference<String> pr = new PhantomReference<String>(new String("Phantom"), rq);
// 从引用队列中弹出一个对象引用
Reference<? extends String> ref = rq.poll();

GC(Garbage Collection)

程序计数器、 虚拟机栈、 本地方法栈3个区域线程私有,这几个区域的内存分配和回收都具备确定性。而Java堆和方法区这两个区域则有着很显著的不确定性,这部分内存的分配和回收是动态的。

GC算法

引用计数算法:在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值就减一; 任何时刻计数器为零的对象就是不可能再被使用的。但是要处理大量边际情况例如对象之间相互循环引用,所以JVM并没有使用引用计数算法。

可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时, 则证明此对象是不可能再被使用的。

GC Roots

枚举根节点时必须停顿所有用户线程,因为根节点集合的对象引用关系在不断变化。

以下可作为GC Roots:

  • 栈中引用的对象, 譬如各个线程被调用的方法堆栈中使用到的参数、 局部变量、 临时变量等。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即通常所说的Native方法) 引用的对象
  • JVM内部的引用,如基本数据类型对应的Class对象, 一些常驻的异常对象(比如NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器
  • 被同步锁(synchronized关键字) 持有的对象

OopMap:在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。

安全点:只在安全点进行GC停顿,只要保证引用变化的记录完成于GC停顿之前就可以

安全区域:在一段代码片段中,引用关系不会发生变化,在该区域的任何地方发生GC都是安全的。

分代收集理论

收集器应该将Java堆划分出不同的区域, 然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数) 分配到不同的区域之中存储。 显而易见, 如果一个区域中大多数对象都是朝生夕灭, 难以熬过垃圾收集过程的话, 那么把它们集中放在一起, 每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象, 就能以较低代价回收到大量的空间; 如果剩下的都是难以消亡的对象, 那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域, 这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

由此Java堆分出了新生代与老年代两个区域。新生代中每次回收会有大批对象死去,而回收时存活的少量对象,会逐步晋升到老年代。

标记-清除算法:先标记需要回收的对象或者存活对象,再进行清除。但会造成空闲内存过于分散。JVM不采纳。

标记-复制算法:内存一分为二,回收时将存活对象复制到另一个,并交换两部分的逻辑角色(From
To)。实际上,JVM的堆中新生代区域按8:1:1的比例划分,其中两个1为Survivor存储存活的对象,8为Eden就是创建对象时默认的内存空间,在平时,有一个Survivor不使用闲置。回收时,先标记需要回收的对象或者存活对象,再进行清除,之后,将所有存活的对象移至闲置的Survivor,然后闲置刚刚使用的Survivor,并且交换两个Survivor的逻辑角色。如果GC年龄到达-XX:MaxTenuringThreshold中要求的年龄,则放进老年代。如果存活对象超出Survivor容量,则将GC年龄大的放进老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 无须等到-XX:MaxTenuringThreshold中要求的年龄。

JVM 新生代为何需要两个 Survivor 空间

标记-整理算法:老年代区域中,每次回收先标记需要回收的对象或存活对象,让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存。

垃圾收集器

Serial收集器:新生代的单线程标记复制。

Serial Old收集器:老年代的单线程标记整理。

ParNew收集器:新生代的并行标记复制。

Parallel Scavenge收集器:新生代的并行标记复制。和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;

吞吐量优先(吞吐量 = 运行用户代码时间 / 运行用户代码时间 + GC时间),高吞吐量可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。所以不适合需要与用户交互或需要保证服务响应质量的程序。

Parallel Old收集器:老年代的并行标记整理。同样注重吞吐量。

CMS收集器:标记清除。以获取最短回收停顿时间为目标的收集器。

  1. 初始标记(CMS initial mark):标记GC Roots能直接关联到的对象,速度很快。需暂停用户线程。
  2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始进行可达性分析。并发运行。
  3. 重新标记(CMS remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的对象的标记记录。需暂停用户线程。
  4. 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象。由于不需要移动存活对象,所以并发运行。

G1 收集器:面向局部收集,基于Region的内存布局。G1 收集器可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代, 而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。它把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要, 扮演新生代的Eden空间、 Survivor空间, 或者老年代空间。 它将Region作为单次回收的最小单元, 即每次收集到的内存空间都是Region大小的整数倍, 这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小, 价值即回收所获得的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表, 每次根据用户设定允许的收集停顿时间(使用参数-XX: MaxGCPauseMillis指定, 默认值是200毫秒) , 优先处理回收价值收益最大的那些Region。

  1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
  2. 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  3. 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。这阶段需要停顿线程,但是可并行执行。
  4. 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

类的回收

只回收废弃的常量和不再使用的类型。

常量回收和引用类似,不存在引用即可回收。

方法区的类型信息的回收很苛刻:

  • 该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景, 如OSGi、 JSP的重加载等, 否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法。

但即使满足了也不一定会回收。

在大量使用反射、 动态代理、 CGLib等字节码框架, 动态生成JSP以及OSGi这类频繁自定义类加载器的场景中, 通常都需要Java虚拟机具备类型卸载的能力, 以保证不会对方法区造成过大的内存压力

性能调优

收集器的选择

追求低延迟,选择CMS、G1收集器。还有其他的低延迟收集器,这里没讲。
追求吞吐量,选择Parallel收集器,可能还有别的吧。
能多线程就选择多线程并行标记回收的收集器。

JDK

我也没想到这也行:升级JDK版本,一般都会带来有效的性能提升。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值