0%

JVM思路大赏

JVM思路大赏。通俗易懂!极简。

JVM思路大赏

[1] 主要参了Guide得 JVM文章,原文作者:说出你的愿望吧丷原文地址:https://juejin.im/post/5e1505d0f265da5d5d744050#heading-28

[2] 参考了cys2018的文章 Java虚拟机

0 前言

我首先要理解JAVA虚拟机是什么、有什么用,然后才能去讨论JVM一些功能的具体实现。

0.1 JAVA虚拟机是什么?

我们知道JDK由编译器、JRE和常用类库组成,而JRE运行时环境是由JVM和核心类库组成的。java文件经由编译器编译成字节码,字节码在虚拟机上运行,所以:JVM其实就是为编译后的class字节码代码提供一种运行的环境(加上核心库)。

说白了,JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下。它直接和操作系统进行交互,与硬件不直接交互,操作系统完成和硬件的交互部分。然后我们写的java代码都运行再JVM提供的环境上,JVM通过在实际的计算机上仿真模拟了一种虚拟的计算机运行环境。

img

0.2 JAVA虚拟机有什么用?

首先我们知道Java语言有以下的几个有优点

  • 它摆脱了硬件平台的束缚,实现了“一次编写,到处运行”的理想;
  • 提供了一个相对安全的内存管理和访问 机制,避免了绝大部分的内存泄露和指针越界问题
  • 它实现了热点代码检测和运行时编译及优化,这使得Java应用 能随着运行时间的增加而获得更高的性能
  • 它有一套完善的应用程序接口,还有无数来自商业机构和开源社区的第 三方类库

那么Java为什么具有以上几个优点呢?答案就再JVM之中!!

1 平台无关性

字节码文件可以在不同机器上的JVM上运行,因此java才具有平台无关性

  • java编译的字节码文件 class文件是平台无关的,class文件再由JVM动态转换就可以变为本地的机器代码。。也就是说虽然JVM是平台有关的(不同操作系统、机器上的jvm版本是不同的),但对开发人员来说是平台无关的。编译生成的字节码文件是可以到处运行的
  • java数据结构的统一性,基本数据类型的大小有明确的规定,比如int永远是32位。。。但c/c++里面可以是16也可以是32

2 内存管理和访问机制

JVM的内存管理、类加载机制和GC也是重点中的重点!!

Java 虚拟机有自动内存管理机制,如果出现内存泄漏和溢出方面的问题,想要排查错误就必须要了解虚拟机是怎样使用内存的。

  • 这里就包括jvm运行时的数据区域、类加载器、垃圾回收等相关内容
  • 内存泄漏的原因和解决

3 热点代码检测和运行时编译及优化

热点代码的解释执行是《深入理解JVM》的第四部分内容,我是菜鸡我还不懂。

  • JIT(Just In Time)。在运行时按需编译的方式就是Just In Time。运行时编译分为两种方式:解释执行和热点方法
  • 解释执行指的是逐条执行。javac把java的源文件翻译成了class文件,而class文件中全都是Java字节码。那么,JVM在加载了这些class文件以后,针对这些字节码,逐条取出,逐条执行,这种方法就是解释执行。
  • 热点方法就是把调用最频繁,占据CPU时间最长的方法找出来将其编译成机器码。让CPU直接执行。这样编出来的代码效率会更高。
  • JIT线程与垃圾回收线程都是守护线程中的一种,守护线程提供一些系统性的功能服务,与普通线程不同,当一个java应用内只有守护线程时,java虚拟机会自然退出。

0.3 java文件是如何在JVM上运行的

比如我们现在写了一个 HelloWorld.java,其实就类似于一个文本文件,只是有一定的缩进而已。

而我们知道java代码需要先经过编译器编译,然后才能在JVM上运行。而运行步骤的第一步就是:通过类加载器将所有的 .class 文件全部搬进JVM里面来

img

接下来就要考虑.class文件放进jvm之后是如何存放的?也就是要了解JVM的内存区域(第一节会详细介绍!!)

首先是各线程所共享的:方法区、堆,然后是各线程私有的:程序计数器、栈。

img

将class文件通过类加载器再在到JVM中,在事实上是将HelloWord.java这个类的纤细加载到了方法区中(这个过程就叫类的加载),然后JVM会找到程序的主入口,执行mian方法。后面需要什么类就加载什么类、进行对象的实例化等等,最后在栈中运行方法!!

其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找(某个类方法表位于方法区该类的类信息中)。

1 内存分配和回收策略

内存区域、内存分配(堆)、回收策略(堆),每一个都是重点

1.1 运行时数据区域【重点】

由各个线程共享的方法区和堆,还有各线程私有的程序计数器和栈。四个区域的功能如下:

  • 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(运行时常量池就在方法区里面,String对象都在里面哦)
  • Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。堆用于存放对象实例,如数组、复杂对象等。
  • 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • 虚拟机栈 这是我们的代码运行空间。我们编写的每一个方法都会放到栈里面运行。局部变量、基本类型的变量、和对象的引用也存放在栈中。

简单来说就是:方法区放类信息、常量和静态变量;然后栈管方法的运行、堆管对象的储存;最后程序计数器,指向下一行需要执行的命令;

1.2 栈和堆的生命周期

虚拟机栈的执行

我们经常说的栈帧数据,说白了在JVM中叫栈帧,放到Java中其实就是方法,它也是存放在栈中的。

栈中的数据都是以栈帧的格式存在,它是一个关于方法和运行期数据的数据集。比如我们执行一个方法a,就会对应产生一个栈帧A1,然后A1会被压入栈中。同理方法b会有一个B1,方法c会有一个C1,等到这个线程执行完毕后,栈会先弹出C1,后B1,A1。它是一个先进后出,后进先出原则。

img

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
    • 线程的栈空间被耗尽,无法创建新的栈帧。(无限的递归调用会产生这个问题)
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。?
    • 请求创建一个超大对象,通常是一个大数组。
    • 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动
    • 过度使用终结器(Finalizer),该对象没有立即被 GC
    • 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收。

栈和堆的生命周期

栈的生命周期和所处的线程是一致的。所以对于栈来说,不存在垃圾回收。只要程序运行结束,栈的空间自然就会释放了。

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。堆内存中存放的是对象,这些对象也就是垃圾回收器主要的回收对象,故堆也称为GC堆。下节将讲如何堆中的对象是如何分配内存的,然后讲如何对其进行回收。

1.3 堆内存的分配【重点】

方法区在JDK7及之前是属于永久代;但JDK8之后,HotSpot 的永久代被彻底移除了,取而代之是元空间,元空间使用的是直接内存。

1 堆内存的分代分配

Java堆内存中划分为年轻代老年代,年轻代又会分为Eden(英[ˈiːdn] 伊甸园)和Survivor区。(Survivor也会分为FromPlaceToPlace,toPlace的survivor区域是空的。Eden,FromPlace和ToPlace的默认占比为 8:1:1。)

img

JVM堆内存结构-JDK8

图示:Eden 区、Survivor 区都属于新生代(这两个 Survivor 区域按照顺序被命名为from和to)

2 Minor GC 和 Full GC

Minor GC 和 Full GC的触发条件

当我们new一个对象后,会先放到Eden划分出来的一块作为存储空间的内存,但是我们知道对堆内存是线程共享的,所以有可能会出现两个对象共用一个内存的情况。这里JVM的处理是每个线程都会预先申请好一块连续的内存空间并规定了对象存放的位置,而如果空间不足会再申请多块内存空间。这个操作我们会称作TLAB,有兴趣可以了解一下。

当Eden空间满了之后,会触发一个叫做Minor GC(英[ˈmaɪnə(r)] 次要的)(就是一个发生在年轻代的GC)的操作,存活下来的对象移动到Survivor0区。Survivor0区满后触发 Minor GC,就会将存活对象移动到Survivor1区,此时还会把from和to两个指针交换,这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。经过多次的 Minor GC后仍然存活的对象会移动到老年代。老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。

而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。

img

Minor GC 和 Full GC的定义

  • Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

1.4 垃圾回收算法【重点】

1 对象是生存还是死亡

判断一个对象是否需要回收

  • 引用计数法:添加引用计数器:当引用他时,计数器值加一,当失效时,减一,计数器为0的对象不可再被使用
    • (很难解决对象之间循环引用的问题)
  • 可达性分析:通过一系列的称为“GC Roots”的 对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
    • 能够解决循环引用的问题,可它的实现需要耗费大量资源和时间(它的分析过程引用关系不能发生变化,所以需要停止所有进程)

宣告一个对象的死亡:至少需要两次标记

  1. 如果对象进行可达性分析之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行finalize()方法。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。
  2. GC对F-Queue队列中的对象进行二次标记。如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。
    • finalize()就是一个对象最后的自救过程
    • finalize()已经不被推荐使用了,对象的四种引用在逐渐代替它的功能

2 再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。Java 提供了四种强度不同的引用类型。

  • 强引用:被强引用关联的对象不会被回收。使用 new 一个新对象的方式来创建强引用。
  • 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。
  • 弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
  • 虚引用:又称为幽灵引用,有没有都一样,唯一作用是回收时可以收到一条系统通知。

3 垃圾回收算法

标记-清除算法

  • 是最基础的收集算法。算法分为“标记”和“清 除”两个阶段:

  • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

  • 缺点

    • 一个是效率问题,标记和清除两个过程的效率都不高
    • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片
img

复制算法

  • 为了解决效率问题的改进,将可用内存按容量划分为两块,每次只使用其中的一块。
  • 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
  • 缺点
    • 这种算法的代价是将内存缩小为原来的一半(也不一定是一半,可指定),堆内存的利用率不高。
img

标记-整理算法

  • 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
img

分代收集算法

  • 一般是把Java堆分为新生代和老年代,这样就可以根据 各个年代的特点采用最适当的收集算法。
  • 新生代一般使用复制算法,(新生代清理频率高)
  • 老年代使用“标记—清理”或者“标记—整理”算法 (老年代清理频率低)

4 垃圾回收器

img

Serial (Old)收集器

Serial 它是单线程的收集器,只会使用一个线程进行垃圾收集工作。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。

它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。

Serial Old是 Serial 收集器的老年代版本,它同样是一个单线程收集器。

img

ParNew收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集。新生代采用标记-复制算法,老年代采用标记-整理算法。同样垃圾回收会停止其他线程。

img

Parallel Scavenge/Old 收集器

Parallel Scavenge 看上去和ParNew 很像,但是其关注点是吞吐量(高效率的利用 CPU)。新生代采用标记-复制算法,老年代采用标记-整理算法。JDK8的默认收集器

Parallel Old是 Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

CMS收集器【重点】

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。CMS收集器是基于“标记—清除”算法实现的。

实现的四个步骤:

  • 初始标记(CMS initial mark)
    • 初始标记仅仅只是标记一下GC Roots能 直接关联到的对象,速度很快
  • 并发标记(CMS concurrent mark)
    • 并发标记阶段就是进行GC RootsTracing的过程,在整个回收过程中耗时最长、不需要停顿 (耗时较长)
  • 重新标记(CMS remark)
    • 重新标记阶段则是为了修正并 发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿
  • 并发清除(CMS concurrent sweep)
    • 需要停顿
img

CMS是一款优秀的收集器,并发收集、低停顿但是CMS还远达不到完美的程度,它有以 下3个明显的缺点:

  • 1、CMS收集器对CPU资源非常敏感。吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  • 2、无法处理浮动垃圾,可能出现 Concurrent Mode Failure。
  • 3、标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配前对象,不得不提前触发一次 Full GC。

G1收集器【重点】

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

img

5 方法区和常量池的回收(了解)

  • 字符串常量池主要回收的是废弃的常量。

    • 如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池了。
  • 方法区:回收无用的类,类需要同时满足下面 3 个条件才能算是 “无用的类”

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

2 类的加载机制

类加载过程、类加载器、双亲委派模型,东西挺多的

[1] 参考了 zenjian_的文章:深入理解java类加载器ClassLoader

[2] 参考了 程序员刘先森的文章:面试官:请你谈谈java的类加载过程

类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。一个类只在首次使用时加载并且仅加载一次。

举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。

2.1 类加载的过程【重点】

类加载的过程主要分为三个部分:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

img

1 加载:

将class文件加载到内存(类加载器),生成一个代表该类的class对象,然后将静态的数据结构转化为方法区中运行时的数据结构(我们知道方法区中保存着类的信息嘛)

2 验证:

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
  • 元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
  • 字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
  • 符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

3 准备:

为类变量(即static修饰的字段变量,静态变量)(在方法区中)分配内存,并且设置该类变量的初始默认值即0或null,如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值

  • 如果类变量是常量(static final修饰),那么它将初始化为表达式所定义的值。比如public static final int j = 123; 初始化为123。
  • 这里也不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中,在创建实例的时候才会初始化

4 解析:

将常量池内的符号引用替换为直接引用的过程。

  • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。(比如 import java.util.ArrayList就是一个全类名的符号引用)
  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

5 初始化:

执行类构造器的过程,目的是将类变量(static静态变量)显式的初始化,比如在准备阶段赋值默认0的变量,此时可以赋值指定的初始值。但要注意:

  • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
  • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

2.2 类加载器的分类

类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。其实类加载器就是在第一个加载阶段工作的。

JVM虚拟机提供了4种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器、自定义类(custom)加载器。

启动(Bootstrap)类加载器

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分。它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中。

扩展(Extension)类加载器

用来加载 Java 的扩展库(jre/ext/*.jar)。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

系统(System)类加载器

它根据 Java 应用的类路径(classpath)来加载 Java 类。一般情况下系统类加载是程序中默认的类加载器。可以通过 ClassLoader.getSystemClassLoader()来获取它。

img

双亲委派模式要求除了BootStrap ClassLoader启动类加载器没有父类加载器之外,其余的类加载其都应当有“父类加载器”,双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,

类加载器之间的关系(了解):

启动类加载器,由C++实现,没有父类。

拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

自定义类加载器,父类加载器肯定为AppClassLoader。

2.3 双亲委派机制【重点】

定义:

类加载器的双亲委派机制:当要加载一个类时,总是先请求父类加载器去处理,也就是说不管哪个类,最后都会委托到BootStrap ClassLoader(启动类加载器)进行加载。

如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。(通俗来讲就是,每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子才会自己想办法去完成,)

优点:

  • 1 Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,即当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 2 考虑到安全因素,使java核心api中定义类型不会被随意替换。比如自己写的Object类并不会替代核心类库中的Object类
    • 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

2.4 类加载的时机【了解】

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

1. 主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

2. 被动引用

以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

  • 通过子类引用父类的静态字段,不会导致子类初始化。
1
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
1
SuperClass[] sca = new SuperClass[10];
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
1
System.out.println(ConstClass.HELLOWORLD);

3 对象创建和内存溢出异常

参考《深入理解Java虚拟机》

3.1 new一个对象

3.1.1 new一个对象的过程【背诵】

1 类加载检查

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

2 在堆区分配对象需要的内存

  • 分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量
  • 分配堆内存有两种方式:分别是 指针碰撞和空闲列表(根据使用何种垃圾回收器而定)
  • 解决分配堆内存时可能出现的并发问题,两种方式解决:CAS+失败重试保证原子性 和 TLAB本地线程分配缓冲

3 将分配到的内存空间初始化为零值(不包括对象头),然后将对象类的元信息、哈希码、GC分代年龄等信息放入对象头中(即JVM设置对象头)

4 最后才将对象初始化,将对象按程序员的意愿赋值,完成对象的创建

5 最后,如果还用对象的引用的话,就在栈区定义引用变量,将堆区对象的地址赋值给它

3.1.2 分配堆内存的方式和并发问题

1 分配堆内存的方式

为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来,目前常用的有两种方式:

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

  • 2.空闲列表(Free List):如果Java堆中的内存并不是规整的,已使用的内存和空间的内存是相互交错的,虚拟机必须维护一个空闲列表,记录上哪些内存块是可用的,在分配时候从列表中找到一块足够大的空间划分给对象使用。

Java堆是否规整由采用何种垃圾回收器决定:

  • 使用Serial、ParNew垃圾回收器时,使用复制算法时一般是指针碰撞
  • 使用CMS基于标记-清除的算法时,一般时空闲列表

2 解决分配堆内存时可能出现的并发问题

除了如何划分可用空间外,在并发情况下划分不一定是线程安全的,有可能出现正在给A对象分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针分配内存的情况,解决这个问题两种方案:

  • 1.分配内存空间的动作进行同步处理:实际上虚拟机采用CAS配上失败重试的方式保证了更新操作的原子性。

  • 2.内存分配的动作按照线程划分在不同的空间中进行:为每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。

3.1.3 对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding).

对象头

  • 1 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word”。

  • 2 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象的哪个实例。

实例数据

对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐填充

对齐填充不是必然存在的,也没有特别的含义,仅仅起着占位符的作用

3.2 内存溢出 out of menmery【重点】

内存溢出(OutOfMemory):没有足够的空间来供jvm分配新的内存块
内存泄露(Memory Leak):已经分配好的内存或对象,当不再需要,没有得到释放

在java虚拟机规范的描述中,除了程序计数器以外,虚拟机内存的其他几个运行时区域都有可能发生内存溢出异常的可能。分别有:Java堆的溢出、虚拟机栈和本地方法栈的溢出、方法区和运行时常量池的溢出、本机直接内存的溢出。

1 Java堆的溢出

定义:

  • Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机 制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

解决:

Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,一般先用工具判断到底是出现了内存泄漏还是内存溢出问题

  • 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。判断哪些对象无法自动回收,定位出泄漏代码的位置
  • 如果不存在泄露,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大

2 虚拟机栈和本地方法栈溢出

原因:

  • 1 栈溢出的原因一般是循环调用方法导致栈帧不断增多,栈深度不断增加,最终没有内存可以分配,出现StackOverflowError。

  • 2 也有可能是建立了过多的线程导致的内存溢出

解决:

  • 如果是单线程递归照成的栈溢出,需要检查代码的循环调用是否有问题
  • 如果是多线程导致的溢出,在不能减少线程数或者更换64位 虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程(即增多栈内存的个数)。

3 方法区和运行时常量池溢出

原因:

  • 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。分配内存不够时会溢出

解决:

  • 手动设置最大永久代(MaxPermSize)大小

4 JVM调优

调优这里暂时就先不看了,先了解一些常用的参数

详细请参考https://juejin.im/post/5e1505d0f265da5d5d744050#heading-28的第四部分

4.1 JVM常用参数

参数名称 含义 默认值 说明
-Xms 初始堆大小 物理内存的1/64(<1GB) 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 物理内存的1/4(<1GB) 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn 年轻代大小(1.4or lator) 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。整个堆大小=年轻代大小 + 老年代大小 + 持久代(永久代)大小.增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize 设置年轻代大小(for 1.3/1.4)
-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
-XX:PermSize 设置永久代(perm gen)初始值 物理内存的1/64
-XX:MaxPermSize 设置永久代最大值 物理内存的1/4
-Xss 每个线程的(堆)栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了

4.1 调整最大堆内存和最小堆内存

-Xmx –Xms:指定java堆最大值 和 初始java堆最小值

(Xmx默认值是物理内存的1/4(<1GB))(Xms默认值是物理内存的1/64(<1GB))

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,默认空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。(40% 70%的比例也是可以指定的)

开发过程中,通常会将 -Xms 与 -Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。

4.2 调整新生代和老年代的比值

-XX:NewRatio — 新生代(eden+2*Survivor)和老年代(不包含永久区)的比值

例如:-XX:NewRatio=4,表示新生代:老年代=1:4,即新生代占整个堆的1/5。在Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

4.3 调整Survivor区和Eden区的比值

-XX:SurvivorRatio(幸存代)— 设置两个Survivor区和eden的比值 (一般8:1:1)

例如:8,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10

4.4 设置年轻代和老年代的大小

-XX:NewSize — 设置年轻代大小

-XX:MaxNewSize — 设置年轻代最大值

4.7 JVM的栈参数调优

4.7.1 调整每个线程栈空间的大小

可以通过-Xss:调整每个线程栈空间的大小

JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右

-------------感谢阅读没事常来-------------