JVM学习笔记

JVM学习笔记

Java内存区域与内存溢出异常

运行时数据区域

程序计数器

程序计数器是一块较小的内存空间.它可以看作是当前线程所执行的字节码的行号指示器. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成.
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器.如果线程正在执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址.

Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,他的生命周期与线程相同.每个Java方法在执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息.每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.
栈中的局部变量表存放了编译期可知的各种基本数据类型(int, long, double,float,short,byte,char,boolean)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向一个代表对象的句柄或其他与此对象相关的位置)、returnAddress类型(指向了一条字节码指令的地址)。

本地方法栈

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

Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。也可以叫做GC堆。
分为新生代,老年代和永久代。新生代又分为伊甸园区,幸存者0区,幸存者1区。

方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的`类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池

是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

直接内存

不是虚拟机运行时数据区的一部分,就是本地的内存。但也会使用到。比如NIO。

对象解密

对象的创建

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。
接下来虚拟机要对对象进行必要的设置,比如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。都存储在对象头中。
到这里,一个新的对象才算创建完成,后面还要根据代码进行初始化。

对象的内存布局

对象在内存中存储的布局可以分为3个区域。对象头,实例数据和对齐填充。
对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。。另一部分是类型指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是Java数组,那么对象头还必须有一块用于记录数组长度的数据。
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类集成的,还是在子类中定义的,都需要记录下来。
第三部分对齐填充并不是必然存在的,仅仅起着占位符的作用。

对象的访问定位

我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。Reference类型的引用包括句柄和直接指针两种。
如果使用句柄的话,那么Java堆中将划分出一块内存区域作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
如果使用直接指针访问,就是直接指向堆中的对象了。

String.intern()

是一个native方法,作用是,如果字符串 常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象。否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

垃圾收集器与内存分配策略

对象已死判断

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它,计数器加一,当引用失效时,计数器减一。任何时刻计数器为0的对象就是不可能再被使用的。

可达性分析算法

通过一系列的“GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到“GC Roots”没有任何引用链相连,则证明此对象不可用。在Java语言中,可作为GC Roots 的对象包括以下几种:

  1. 虚拟机栈中引用的对象
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(即一般说的native方法)引用的对象。

Java中的引用

强引用

代码中普遍存在的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象.

软引用

描述一些还有用,但并非必须的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围内进行二次回收。

弱引用

被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用

约等于无。

生存还是死亡

宣判死刑要经历两次标记过程。如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那他将会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,从而直接回收。
如果有必要执行,就会被放置到F-Queue的队列中,如果对象在finalize()方法中成功拯救自己,那么就移除队列,否则就真正的被回收了。
任何一个对象的finalize()方法都只会被系统自动调用一次。

回收方法区

判断一个类是否是无用类的标准有三个:

  1. 该类的所有实例都已经被回收。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机会对无用类进行回收。

垃圾收集算法

标记-清除算法

算法分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

复制算法

把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

标记-整理算法

标记过程仍与“标记-清除” 算法一样,但后续步骤 不是直接对可回收对象进行清理。而是让所有存货对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

根据对象存活周期的不同将内存划分为几块。把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都会有大量的对象死去。只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有对象对他进行担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

垃圾收集器

Serial收集器

当他进行垃圾收集时,其他的线程都必须暂停。等他收集完毕再继续。

ParNew收集器

Serial收集器的多线程版本。使用多条线程进行垃圾回收。

Parallel Scavenge收集器

是一个新生代收集器,使用复制算法,同时是并行的。

Serial old 收集器

Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法。

Parallel Old 收集器

Parallel Scavenge 收集器的老年版本,使用多线程和“标记-整理”算法。

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现的。整个过程分为4个步骤,初始标记,并发标记,重新标记,并发清除。
初始标记和重新标记仍然需要暂停所有进程。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快。并发标记阶段就是进行GCRootsTracing的过程。而重新标记阶段则是为了修正并发标记期间因用户程序持续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发标记和并发清除都可以与用户线程一起工作。

G1收集器

最先进的收集器,有以下优点

  1. 并行与并发
  2. 分代收集
  3. 空间整合
  4. 可预测的停顿

分为4个步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

前三步与CMS一样,最后一步首先对Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。

内存分配和回收策略

对象优先在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC(新生代GC)。注: Full GC是老年代。

大对象直接进入老年代

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

虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次MinorGC后仍然存在,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每“熬过”一次GC,年龄就增加一岁。增加到一定程度后,就将会被晋升到老年代中。

动态对象年龄判定

如果在Survivor空间中相同年龄所有对象的综合总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

虚拟机类加载机制

类加载的整个声明周期包括:加载,验证,准备,解析,初始化,使用和卸载。 验证,准备,解析三个阶段统称为连接。
触发初始化的五种场景:

  1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时。
  2. 使用反射包的方法对类进行反射调用时。
  3. 当初始化一个类的时候发现其父类还没初始化,则会先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类。
  5. 当使用JDK1.7的动态语言支持时。。方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

类加载的过程

加载

在加载阶段,虚拟机需要完成以下3件事情。

  1. 通过一个类的完全限定名来获取此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。并且不会危害虚拟机自身的安全。验证阶段大致会完成下面4个阶段的检验动作。

  1. 文件格式验证。验证通过才会进入方法区进行存储。
  2. 元数据验证。
  3. 字节码验证。
  4. 符号引用验证

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里的变量仅包括类变量。 这里的初始值也是数据类型的零值。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

类初始化是类加载过程中的最后一步.前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

类加载器

双亲委派模型

Java虚拟机只存在两种不同的类加载器,一种是启动类加载器,使用CPP实现,另一个就是其他的类加载器,由Java实现。

  1. 启动类加载器
  2. 扩展类加载器
  3. 应用程序加载器

我们的应用程序都是由这3种加载器相互配合进行加载的。还可以加入自己定义的加载器。
双亲委派要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

Java内存模型与线程

JVM调优

默认jvm内存占计算机的1/4,通过-Xms设置最小值,-Xmx设置最大值。然后jvm的回收日志,一般这种格式 回收前-》回收后(总大小)。

JMM java内存模型

是一组规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
三个特征 可见,原子,有序。

posted @ 2019-12-30 10:43  时光轻轻吹  阅读(155)  评论(0编辑  收藏  举报