JVM学习一

学习JVM目的

如何对JVM进行内存分配,应该对当前服务采用哪种垃圾回收器的问题。当我们遇到OOM内存溢出的故障时,也必须去分析研究其原因,所以还需要使用到各个内存调优分析手段。

JVM内存的分区

重点掌握堆内存,栈内存以及方法区域的定义和作用
在这里插入图片描述
方法区:方法区是一个线程之间共享的区域常量静态变量以及JIT编译后的代码都在方法区。主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过**-XX:MaxPermSize**控制上限。

堆内存:堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例,通过-Xmx 和-Xms 可以控制大小。

虚拟机栈(栈内存):栈内存中主要保存局部变量基本数据类型变量以及堆内存中某个对象的引用变量。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。

程序计数器: 程序计数器是当前线程执行的字节码的位置指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

本地方法栈: 主要是为JVM提供使用native 方法的服务。

对象创建过程中的内存分配

我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。

通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。

对象的内存分配有两种方式,即指针碰撞和空闲列表方式。

  • 指针碰撞方式:
    假设Java堆中的内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,那么所有的内存分配就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
  • 空闲列表方式:
    如果Java堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。

Java 堆内存是否规整是由所使用的垃圾收集器是否拥有压缩整理功能来决定的

内存的分配保证线程安全问题

  • 对分配内存空间的动作进行同步处理,通过“CAS + 失败重试”的方式保证更新指针操作的原子性
  • 把分配内存的动作按照线程划分在不同的空间之中,即给每一个线程都预先分配一小段的内存,称为本地线程分配缓存(TLAB),只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。 虚拟机是否使用TLAB,可以通过-XX: +/-UserTLAB参数来设定

怎么找到要访问的对象的?

  • 句柄访问方式:
    在JVM的堆内存中划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。在内存垃圾收集之后,对象会移动,但是引用reference中存储的是稳定的句柄地址,但是句柄地址方式不直接,访问速度较慢。

  • 直接指针访问方式:
    引用变量中存储的就是对象的直接地址,通过指针直接访问对象。直接指针的访问方式节省了一次指针定位的时间开销,速度较快。Sun HotSpot使用了直接指针方式进行对象的访问。

JVM内存分配和垃圾回收

JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代。年轻代又可以进一步划分为一个Eden(伊甸)区两个****Survivor(幸存)区组成。

JVM堆内存的分配:

JVM初始分配的堆内存由**-Xms指定,默认是物理内存的1/64。JVM最大分配的堆内存由-Xmx**指定,默认是物理内存的1/4。

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。

通过参数**-Xmn2G** 可以设置年轻代大小为2G。通过**-XX:SurvivorRatio**可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域。

JVM非堆内存的分配

JVM使用**-XX:PermSize** 设置非堆内存初始值,默认是物理内存的1/64。由**-XX:MaxPermSize**设置最大非堆内存的大小,默认是物理内存的1/4。

堆内存上对象的分配与回收

我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个-XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝。

另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过
-XX:MaxTenuringThreshold设置晋升年龄。

堆内存上的对象回收也叫做垃圾回收,那么垃圾回收什么时候开始呢?
垃圾回收主要是完成清理对象,整理内存的工作。上面说到GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC,分别介绍如下。

Minor GC(年轻代GC):
对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。

Full GC(老年代GC):
Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。

动态对象年龄判定:
如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold。

空间分配担保:
发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC

如何判定一个对象是否应该被回收?

判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法

引用计数法:
是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

root根搜索方法:
root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。以下对象会被认为是root对象:

  • 栈内存中引用的对象
  • 方法区中静态引用和常量引用指向的对象
  • 被启动类(bootstrap加载器)加载的类和创建的对象
  • Native方法中JNI引用的对象。

对象的引用

  • 强引用:普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。
  • 弱引用:通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
  • 虚引用:也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值