0、JVM
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
x、Java强引用,软引用,弱引用,虚引用JAVA 强引用
强引用
在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用
Java研发军团弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
1、JVM的体系结构
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存
程序计数器
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有” 的内存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
Java栈
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈
本地方法栈和 Java Stack 作用类似, 区别是Java栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 Clinkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一 。
堆(Heap-线程共享) -运行时数据区
是被线程共享的一块内存区域, 创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。 由于现代VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代
方法区/永久代(线程共享)
即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据.HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)
2、JVM的作用:
作用:解释运行字节码程序 消除平台相关性
jvm 将 java 字节码解释为具体平台的具体指令。一般的高级语言如要在不同的平台上运行,至少需要编译成不同的目标代码。而引入 JVM 后,Java 语言在不同平台上运行时不需要重新编译。Java 语言使用模式 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
3、ClassLoader(类加载器)
1. 类加载的定义
JVM 中类的装载是由 ClassLoader 和它的子类来实现的,它的工作就是把 class 文件从硬盘读取到内存中,在JVM运行时数据区的堆区中创建对应的 java.lang.Class 对象。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
2. 类装载方式
- 隐式装载, 程序在运行过程中当碰到通过 new 等方式生成对象时,隐式调用类装载器加载
对应的类到 jvm 中, - 显式装载, 通过 class.forname()等方法,显式加载需要的类 , 隐式加载与显式加载的
区别:两者本质是一样的。
Java 类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到 jvm 中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
3. 类加载过程
(1)加载:在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
(2)链接:将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。
- ① 验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求(不会危害虚拟机的安全),包括有文件格式验证、原数据验证、字节码验证,符号引用验证等。
- ② 准备:正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。为静态变量赋0值。如果是final的那么直接赋值。
- ③ 解析:将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
假如A类引用了B,在A类进行加载的时候A不知道B是否被加载过了,所以A此时不知道B的实际地址,只能使用一个字符串来代替B的地址,这就是符号引号。A到解析阶段会发现B还未被加载,此时会加载B,此时A中的符号引用会被替换为B的实际地址,这就是直接引用。
如果A调用的B是一个具体的实现类,那就是静态解析。但是如果B使用了多态,B只是一个抽象类或者接口,那么此时A仍然不知道B到底会被谁调用,此时只能等一等,直到B被具体调用了,A才能用直接引用替换符号引用
(3)初始化
类加载过程的最后一步,到了这个阶段才真正开始执行类中定义的Java程序代码。
对主动资源初始化操作进行赋值,例如对于类层面的类的成员变量,静态变量,静态代码块。而对象层面的调用new指令执行构造函数对对象进行实例化是不执行的。
注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
4. 类加载器的分类
-
Bootstrap classLoader: 启动类(根)加载器,主要负责加载核心的类库(java.lang.*等)。
-
ExtClassLoader: 扩展类加载器,主要负责加载jre/lib/ext目录下的一些扩展的jar。
-
AppClassLoader: 应用程序类加载器,主要负责加载应用程序的主函数类
4、双亲委派机制
4.1、介绍
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
当我们自定义一个类的时候,类加载器收到类加载的请求,将这个请求向上委托给父类加载器去完成,按照应用程序类加载器到扩展类加载器一 直向上委托直到启动类加载器。启动加载器检查是否能够加载当前这个类,能加载就结束, 使用当前的加载器,否则, 抛出异常,通知子加载器进行加载。
例如自定义一个java.lang包下的string类,你执行的时候是不会执行你自定义的这个类的,因为类加载器往上找发现在根加载器上也有一个java.lang.string,这个时候就会加载这个类。
有一个特别好的比喻说,双亲委派机制就像啃老族,只有父母没有才自己加载。
4.2、好处
这种设计有个好处是,如果有人想替换系统级别的类,篡改它的实现是行不通的,保证了安全。
4.3、如何打破
-
自定义类加载器,重写loadClass方法;
-
使用线程上下文类加载器
5、Native
native :
凡是带了native关键字的,说明java的作用范围达不到了,回去调用底层c语言的库,会进入本地方法栈 ,调用本地方法本地接口JNI (Java Native Interface)
JNI作用:
开拓Java的使用,融合不同的编程语言为Java所用。
Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序,它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法,在最终执行的时候,加载本地方法库中的方法通过JNI。
Native Method Stack
它的具体做法是Native Method Stack中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies(本地库)
6、堆(Heap)
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
堆内存中还要细分为三个区域:
-
新生区:包括伊甸园区,幸存from区,幸存to区。
- 所有的对象都是在伊甸园区new出来的
- 伊甸园满了就触发轻GC,经过轻GC存活下来的就到了幸存者区,幸存者区满之后意味着新生区也满了,则触发重GC,经过重GC之后存活下来的就到了养老区
-
养老区
- 假设养老区内存满了,会报OutOfMemoryError(OOM),堆内存不够
-
永久区
- 这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境
- 这个区域不存在垃圾回收,关闭虚拟机就会释放内存
- 在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入nativememory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
7、性能调优
- 设定堆内存大小
-Xmx:堆内存最大限制。 - 设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比 - 设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
8、问题排查
对于还在正常运⾏的系统:
- 可以使⽤jmap来查看JVM中各个区域的使⽤情况
- 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
- 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏调优了
- 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析
- ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效
- 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存
对于已经发⽣了OOM的系统:
- ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
- 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
- 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
- 然后再进⾏详细的分析和调试
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题