JVM

运行时数据区

  • 方法区:存储被加载的类信息、常量、静态变量。和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

  • 堆:所有new出来的对象,-Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小

  • 栈:

    1. 本地方法栈:本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
    2. 虚拟机栈:线程创建时产生,方法执行生成栈帧 -Xss
  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
    栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

  • 程序计数器:记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)

垃圾收集发生的时机?

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。 当然,我们可以手动进行垃圾回收, 比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说 System.gc()只是通知要回收,什么时候回收由JVM决定。 但是不建议手动调用该方法,因为消耗的资源比较大

一般以下几种情况会发生垃圾回收:

  • 当Eden区或者S区不够用了
  • 老年代空间不够用了
  • 方法区空间不够用了
  • System.gc()

内存泄漏与内存溢出的区别

  • 内存泄漏:对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
  • 内存溢出:内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。

GC ROOT 有哪些?

  1. 两个栈: Java栈 和 Native 栈中所有引用的对象;
  2. 两个方法区:方法区中的常量和静态变量;
  3. 所有线程对象;
  4. 所有跨代引用对象;如果老年代的 Old 对象,引用了年轻代的 Young 对象,在对年轻代进行可达性分析时,Old 对象算作 GC Root。这样就不用遍历老年代了。
  5. 和已知 GCRoots 对象同属一个CardTable 的其他对象。:分代回收算法需要有一个表,用来记录所有的跨代引用,很耗内存。HotSpot 使用 CardTable 记录老年代对年轻代的引用。把老年代按照 4KB 的大小分块,每一块对应在 CardTable 中都是1 bit。当值为1时,表示这4KB 的内存中有对年轻代的引用,需要加入到 GC Roots 中。

young gc会有stw吗?

不管什么 GC,都会有 stop-the-world,只是发生时间的长短。

major gc和full gc的区别

  • major gc指的是老年代的gc。
  • full gc等于young+old+metaspace的gc。

G1与CMS的区别是什么

CMS 用于老年代的回收,而 G1 用于新生代和老年代的回收。G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生。

什么是直接内存

直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。

不可达的对象一定要被回收吗?

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对 象死亡,至少要经历两次标记过程;

可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此 对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机 将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关 联,否则就会被真的回收。

方法区中的无用类回收 方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满 足下面 3 个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然 被回收。

不同的引用

JDK1.2以后,Java对引用进行了扩充:强引用、软引用、弱引用和虚引用

  • 强引用:我们用的都是基本上上都是强引用。这些引用不会被垃圾回收,当内存空间不足时抛出OOM错误。终止程序。

  • 软引用:软引用在java中用java.lang.ref.SoftReference类来表示,只有在JVM内存不足的时候才会回收该对象。一般用来实现缓存:网页缓存,图片缓存等。

  • 弱引用:在java中,用java.lang.ref.WeakReference类,当发生垃圾回收的时候,一旦发现弱引用对象,无论内存是否充足,都会进行回收。

  • 虚引用:在java中用java.lang.ref.PhantomReference类表示,主要用来追踪对象被垃圾回收的活动的,回收情况都是未知的。

引用类型 被回收时间 用途 生存时间
强引用 从来不会 一般对象 jvm停止运行
软引用 内存不足 对象缓存 内存不足
弱引用 jvm垃圾回收 对象缓存 gc运行后
虚引用 未知 未知 未知

JVM类加载

  1. 加载: 查找和导入class文件,
    • 通过一个类的全限定名获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
  2. 链接
    • 验证:保证被加载类的正确性,文件格式验证,元数据验证,字节码验证,符号引用验证
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的符号引用转换为直接引用
  3. 初始化:对类的静态变量,静态代码块执行初始化操作。

类装载器ClassLoader

  1. Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或 Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。
  2. Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中 jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
  3. App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和 jar包。
  4. Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据 自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

加载原则:

检查某个类是否已经加载:顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。 加载的顺序:加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

双亲委派机制

定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把 这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

优势:Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的 Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型 最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用 双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。

破坏:可以继承ClassLoader类,然后重写其中的loadClass方法,其他方式大家可以自己了解拓展一下。

内存模型

一块是非堆区,一块是堆区。

堆区分为两大块:一个是Old区,一个是Young区。

Young区分为两大块:一个是Survivor区(S0+S1),一块是Eden区。

Eden:S0:S1=8:1:1 S0和S1一样大,也可以叫From和To。

如何理解Minor/Major/Full GC?

  • Minor GC:新生代
  • Major GC:老年代
  • Full GC:新生代+老年代

为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。 这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。 老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。 执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。

可能你会说,那就对老年代的空间进行增加或者较少咯。 假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长。

假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。

所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16 次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个Survivor区?

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设 现在只有一个Survivor区,我们来模拟一下流程:

刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循 环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的 存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

如何确定一个对象是垃圾?

  1. 引用计数法

    对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任 何指针对其引用,它就是垃圾。

弊端 :如果AB相互持有引用,导致永远不能被回收。

  1. 可达性分析

通过GC Root的对象,开始向下寻找,看某个对象是否可达
能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

垃圾收集算法

  • 标记-清除(Mark-Sweep)

    标记: 找出内存中需要回收的对象,并且把它们标记出来此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时。
    清除:清除掉被标记需要回收的对象,释放出对应的内存空间。

  • 复制(Copying)

    将内存划分为两块相等的区域,每次只使用其中一块,当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次 清除掉。
    缺点: 空间利用率降低。

  • 标记-整理(Mark-Compact)

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

  1. Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
  2. Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

垃圾收集器

  • Serial收集器:Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。

    它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更 重要的是其在进行垃圾收集的时候需要暂停其他线程。

  1. 优点:简单高效,拥有很高的单线程收集效率
  2. 缺点:收集过程需要暂停所有线程 算法:复制算法
  3. 适用范围:新生代 应用:Client模式下的默认新生代收集器
  • ParNew收集器:

    可以把这个收集器理解为Serial收集器的多线程版本。

  1. 优点:在多CPU时,比Serial效率高。
  2. 缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
  3. 算法:复制算法
  4. 适用范围:新生代 应用:运行在Server模式下的虚拟机中首选的新生代收集器
  • Parallel Scavenge收集器:

    Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注 系统的吞吐量。

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)

比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。

若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序 的运算任务。

-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,

-XX:GCTimeRatio直接设置吞吐量的大小。

  • Serial Old收集器:

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用”标记-整理算 法”,运行过程和Serial收集器一样。

  • Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和”标记-整理算法”进行垃圾回收。

吞吐量优先。

  • CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。

  • 优点:并发收集、低停顿
  • 缺点:产生大量空间碎片、并发阶段会降低吞吐量
    采用的是”标记-清除算法”,整个过程分为4步
  1. 初始标记 CMS initial mark ->速度很快 标记GC Roots能关联到的对象 Stop The World–
  2. 并发标记 CMS concurrent mark 进行GC Roots Tracing
  3. 重新标记 CMS remark 修改并发标记因用户程序变动的内容 Stop The World
  4. 并发清除 CMS concurrent sweep

由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

  • G1收集器
    G1特点:
  1. 并行与并发
  2. 分代收集(仍然保留了分代的概念)
  3. 空间整合(整体上属于“标记-整理”算法,不会导致空间碎片) 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集 上的时间不得超过N毫秒)

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个 大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再 是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

发生FULL GC 的时候是CPU消耗还是内存消耗的多

CPU消耗的多

对于JVM来说,内存的大小是固定的,发生full GC的主要在于线程去标记和去清理,所以会消耗线程,发生STW,也就导致了其它线程的访问阻塞。所以CPU会升高。

触发FullGC

  • 老年代空间不足

    如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。

  • 持久代空间不足

    如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC

  • YGC出现promotion failure

    promotion failure发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC.

  • 统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间

    在发生YGC是会判断,是否安全,这里的安全指的是,当前老年代空间可以容纳YGC晋升的对象的平均大小,如果不安全,就不会执行YGC,转而执行Full GC。

  • 显示调用System.gc

如何选择合适的垃圾收集器

官网 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28

  1. 优先调整堆的大小让服务器自己来选择
  2. 如果内存小于100M,使用串行收集器
  3. 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
  4. 如果允许停顿时间超过1秒,选择并行或JVM自己选
  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器 对于G1收集

常用命令

  • jps:查看java进程的id号

  • jinfo:实时查看和调整JVM配置参数,jinfo -flag

  • jstat:

    1. 查看虚拟机性能统计信息
    2. 查看类装载信息
      jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10 次
    3. 查看垃圾收集信息
      jstat -gc PID 1000 10
  • jstack
    查看线程堆栈信息

  • jmap

    1. 生成堆转储快照

    2. 打印出堆内存相关信息
      -XX:+PrintFlagsFinal -Xms300M -Xmx300M
      jmap -heap PID

    3. dump出堆内存相关信息
      jmap -dump:format=b,file=heap.hprof PID
      jmap -dump:format=b,file=heap.hprof 44808

    4. 要是在发生堆内存溢出的时候,能自动dump出该文件就好了
      一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件
      -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof


文章作者: 凌云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 凌云 !
  目录