《深入理解Java虚拟机》读书笔记

250 阅读8分钟

JVM内存区域

  • 程序计数器:记录当前线程执行的字节码文件的行号
  • 栈:局部变量表、操作栈、方法出口等,局部变量存放基本类型数据,对象引用等,方法调用到执行完,对应一个栈帧在虚拟机中从入栈到出栈。
  • 本地方法栈:为虚拟机使用到的Native方法服务
  • 堆:是JVM管理的内存中最大的一块,存放对象实例、数组等。
  • 方法区:存放类信息、静态变量、常量(常量池)、编译后的代码

OutOfMemoryError异常

除了程序计数器,其他区域都有可能发生OutOfMemoryError异常。

  • 栈:当线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflow异常,栈扩展时无法申请到足够内存时,将抛出OutOfMemoryError异常。
  • 堆:如果堆内存无法完成实例分配,且无法扩展时,将抛出OutOfMemoryError异常。

如何解决? 栈溢出:Xss设置栈容量 堆溢出:内存分析工具判断是内存泄露还是内存溢出; 内存泄露:定位出泄露代码位置; 内存溢出:检查虚拟机堆参数,同时检查代码(是否存在某些对象生命周期过长等)。通过Xms和Xmx设置堆的最小值、最大值。

内存泄露:指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。Java中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾。当我们new了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这会造成内存泄露。 内存溢出:指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。

垃圾回收

JVM内存区域的堆和方法区是线程共享的,程序计数器、栈、本地方法栈是线程独立的。

  • 线程独立区域:随线程而生,线程结束时,内存也跟着回收了,不需要过多考虑垃圾回收的问题。
  • 线程共享区域:类、方法需要的内存不一样,只有运行时才知道需要创建哪些对象,内存分配和回收是动态的,需要关注垃圾回收。

垃圾回收需要完成 3件事情:

  • 哪些内存需要回收?
  • 什么时候回收
  • 如何回收

哪些内存需要回收?如何判断对象是否存活?

  • 引用计数法:存在循环引用问题
  • 根搜索算法:当一个对象到GC Roots没有任何引用即不可达时,将进行第一次标记和筛选,如果这个对象有必要执行finalize方法(对象可以拯救自己),之后再进行一次标记筛选,未逃脱的对象将被回收。
  • 可以作为GC Roots的对象包括以下几种:
    • 虚拟机栈中引用的对象
    • 方法区中类变量引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈引用的对象

Java中的四种引用

  • 强引用:对象存在强引用,则不会被回收。 Object obj = new Object()
  • 软引用:用于描述有用但非必须的对象,如果将发生内存溢出,列入回收范围。 SoftReference softReference = new SoftReference(student);
  • 弱引用:非必须对象,只能活到下一次垃圾回收之前。 WeakReference weakReference = new WeakReference(student);
  • 虚引用:任何时候都可能被回收,置虚引用的目的是为了被虚引用关联的对象在被垃圾回收器回收的时候,能够收到一个系统通知(用来跟踪对象被GC回收的活动)。 PhantomReference phantomReference = new PhantomReference(object, queue);

什么时候回收?

  • 应用程序空闲时
  • Java堆内存不足时

如何回收?

垃圾回收算法

  • 标记-清除算法:标记需要回收的对象,清除标记的对象。缺点:效率低,内存碎片。
  • 复制算法:内存划分为2块,每次只使用一块,当一块用完了,将存活的对象复制到另外一块,再全部清除已使用的内存块。缺点:内存缩小一半。
  • 标记-整理算法:标记需要回收的对象,将所有存活的对象向一端移动,再将端以外的清除。
  • 分代收集算法:年轻代,对象朝生夕死,每次垃圾回收存活对象少,适合复制算法。老年代,对象存活率高,适合标记清除/标记整理算法。

垃圾收集器

新生代收集器,采用复制算法,需要stop the world暂停所有用户线程

  • Serial收集器:串行收集器
  • ParNew收集器:并行收集器
  • Parallel Scavenger收集器:并行收集器,关注吞吐量(运行用户代码时间/CPU消耗时间)

老年代收集器

  • Serial Old收集器:标记整理算法,串行收集器,需要stop the world。
  • Parallel Old收集器:标记整理算法,并行收集器,需要stop the world。
  • CMS收集器:标记清除算法,垃圾收集线程和用户线程可以并发执行,运作过程:
    • 初始标记:标记直接与GC root相连的对象,stop the world
    • 并发标记:标记到GC root可达的对象
    • 重新标记:并发标记期间产生变动的对象,stop the world
    • 并发清除:清除标记区域 缺点:
    • 对CPU资源非常敏感,默认的垃圾回收线程数为(CPU数量+3)/4
    • 无法处理浮动垃圾,并发清除过程产生的垃圾
    • 标记清除算法,产生大量空间碎片
  • G1收集器,运作过程:
    • 初始标记:标记直接与GC root相连的对象,stop the world
    • 并发标记:标记到GC root可达的对象
    • 最终标记:并发标记期间产生变动的对象,stop the world
    • 筛选回收:清除标记区域,与用户线程可以并发执行,但垃圾回收时间用户可控制,所以也是stop the world
    • 优点:
      • 分代回收,可以管理新生代和老年代
      • 不会产生内存空间碎片,从整体看是标记整理算法,局部看是复制算法
      • 可预测停顿时间:将Java堆分成多个区域,跟踪区域的垃圾堆积程度,并维持一个优先列表,每次根据垃圾回收时间,优先回收垃圾最多的区域。

MinorGC和MajorGC

  • MinorGC:新生代垃圾回收,非常频繁。eden区没有足够空间分配时,发起一次MinorGC
    • 在发生minorGC时,先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于则minorGC安全。如果小于,继续检查老年代最大可用的连续空间是否大于之前每次晋升到老年代的平均大小,如果大于,则尝试进行一次minorGC,如果大于,则进行一次majorGC。
  • MajorGC:老年代垃圾回收,出现MajorGC会伴随一次MinorGC,比MinorGC慢10倍以上

新生代和老年代

  • 新生代:eden+survivor0+survivor1(默认8:1:1)
  • 老年代:大对象如长字符串、长数组直接分配在老年代,长期存活的对象进入老年代。(存活时间:每个对象定义一个对象年龄计数器,每次minorGC后仍能存活,放入survivor1,对象年龄加1,当年龄增加到15(默认配置),晋升到老年代)。

常用的JVM配置参数

  • Xms最小堆大小
  • Xmx最大堆大小
  • Xss每个线程的栈大小
  • Xmn年轻代大小
  • XX:SurvivorRatio Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
  • XX:PretenureSizeThreshold对象超过多大是直接分配在老年代

类加载过程

  • 加载:通过类的全限定名加载类的二进制字节流
  • 验证:检查class文件字节流包含的信息是否符合jvm的要求,保证jvm的安全
  • 准备:给类变量分配内存,并设置初始值
  • 解析:将常量池的符号引用替换为直接引用
    • 符号引用:是一个常量,比如class文件中用CONSTANT_Class_info表示类或接口
    • 直接引用:将常量解析为对应的类,方法、字段等
  • 初始化:初始化类变量、静态代码块等。

类加载器

类加载器源码

  • 检查指定名称的类是否已经加载过,如果加载过了,就不需要再加载
  • 如果此类没有加载过,那判断是否有父加载器;如果有父加载器,则由父加载器加载。如果没有父加载器,调用启动类加载器来加载。
  • 如果没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。默认的findclass直接抛出ClassNotFound异常,所以我们自定义类加载器要覆盖这个方法。 可以猜测:ApplicationClassLoader的findClass是去classpath下去加载,ExtentionClassLoader是去java_home/lib/ext目录下去加载,实际上就是findClass方法不一样罢了。

双亲委派模型

如果一个类加载器收到类加载的请求,先将请求委派给父类加载器去加载,所有的类加载请求都应该传送到顶层的启动类加载器中,只有当父加载器无法加载时(搜索范围中没有找到所需的类),子加载器才会尝试去加载。

自定义类加载器

如果不想打破双亲委派模型,那么只需要重写findClass方法; 如果想打破双亲委派模型,那么就重写整个loadClass方法。

破坏双亲委派模型

双亲委派模型不是强制约束,通过重写整个loadClass方法可以破坏。