文章目录
- 类加载器
- 内存布局
- 本地方法栈
- 程序计数器
- JVM Stack(虚拟机栈)
- Heap(堆区)
- 垃圾收集
- JVM常用参数
- 常见的OOMError
类加载器
类加载器把字节码文件(.class文件)加载进内存中的方法区,方法区存储了类的模板(Class类)
加载器种类
Java自带的虚拟机加载器
- 启动类加载器 Bootstrap
也被称为根加载器,用来加载Java运行所必需的的类,比如rt.jar下的Object类
- 扩展类加载器 Extension(JDK1.9 Platform)
主要是加载javax包下的类,用来加载Java扩展的功能:大数据云计算之类
- 应用程序类加载器AppClassLoader
加载自定义的类
Java自带的加载器只有上面三种,根加载器加载级别最高,应用程序加载器级别最低。我们还可以通过实现抽象类ClassLoader
来自定义加载器,那么一般什么情况需要自定义加载器呢?
- 隔离加载类
在某些框架内进行中间件与应用模块的隔离,把类加载到不同的环境
- 修改类的加载方式
Java类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者可以根据实际情况在某个时间点按需进行动态加载
- 扩展加载源
- 防止源码泄露
双亲委派(溯源加载)
低级别的类加载器,不能覆盖高级别类加载器已经加载的类。一个低级别的类加载器如果想要加载一个类,需要向上级询问:它是否可以加载?上级需要回答两个问题:1.我是否已经加载此类。2.我是否可以加载此类。这两个问题都是否子类加载器才可以加载。高级别类加载器已经加载的类不能覆盖,高级别类加载器可以加载的类由高级别类加载器进行加载,举个例子:我们自己定义了一个类java.lang.String
不会被加载。这种机制杜绝了不安全类的加载,个人代码不会污染Java源代码,通过双亲委派机制来保证沙箱安全(防止恶意代码污染Java源代码)
内存布局
本地方法栈
Java中被native
被称为本地方法,这些方法是使用C/C++编写的,Java专门在内存中开辟了一块区域用来管理这些方法的运行
程序计数器
程序计数器(PC寄存器)是一个指针,指向方法区中的方法字节码,用来记录程序方法的调用和执行顺序。
字节码的执行原理
编译后的字节码在没有经过JIT(实时编译器)编译前,是通过字节码解释器进行解释执行。其执行原理为:字节码解释器读取内存中的字节码,按照顺序读取字节码指令,读取一个指令就将其翻译成固定的操作,根据这些操作进行分支,循环,跳转等动作。
程序计数器的作用
从字节码的执行原理来看,单线程的情况下程序计数器是可有可无的。因为即使没有程序计数器的情况下,程序会按照指令顺序执行下去,即使遇到了分支跳转这样的流程也会按照跳转到指定的指令处继续顺序执行下去,是完全能够保证执行顺序的。但是现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器。如果执行的是一个native方法,那么计数器是空的(native方法不归Java管)
JVM Stack(虚拟机栈)
虚拟机栈是描述Java方法执行的内存区域,在线程创建时创建,线程私有。它的生命周期是跟随线程的生命期,线程结束栈内存也就释放,不存在垃圾回收问题,但递归等方法可能会出现栈溢出(StackOverflowError)
栈帧
栈帧是栈运行的基本结构,每个方法执行都会创建一个栈帧,一个方法从调用到完成对应了栈帧在栈中入栈和出栈的过程。只有位于栈顶的帧才是有效的,称为当前栈帧,正在执行的方法被称为当前方法。栈帧包含局部变量表、操作数栈、动态连接、方法出口等
- 局部变量表
局部变量表存储了方法参数和局部变量,必须显示初始化。如果方法是非静态的,在index[0]的位置上存储的是方法所属对象的实例引用(调用这个方法的对象的地址,指向了堆区)
- 操作栈
操作栈是一个初始状态为空的桶式结构栈,方法执行过程中,各种指令往栈中写入和提取信息。JVM的执行引擎是基于栈的执行引擎,栈指的就是操作栈。从局部变量表取出元素,压入操作栈中,但有的指令可以直接操作局部变量表比如iinc
- 动态链接
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接
- 方法返回地址
方法执行中有正常退出和异常退出两种情况。无论何种退出方式,都将返回至方法当前被调用的位置。方法的退出相当于弹出当前栈帧,退出可能有三种方式
- 返回值压入上层调用栈帧
- 异常信息抛给能够处理的栈帧
- PC计数器指向方法调用后的下一条指令
Heap(堆区)
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的默认是计算机实际内存的1/4。堆区存储着几乎所有的实例对象。堆内存逻辑上分为三个部分:新生代、老年代、元空间,物理上有两个部分:新生代、老年代
- Heap
- 新生代(Young/New)占整个堆区大小的1/3
- 伊甸园区(Eden)占整个新生代大小的8/10
- 幸存者0区(Survivor0)占整个新生代大小的1/10
- 幸存者1区(Survivor1)
- 老年代(Old/Tenure)占整个堆区大小的2/3
- 永久代(元空间)1.7叫永久代,1.8之后改为叫元空间。元空间并不在虚拟机中,改为使用本机物理内存
- 一个对象被创建在新生代的Eden区,如果Eden区放不下会直接送入老年代。当Eden区被填满的时候,会触发一次YGC(Young Garbage Collection),YGC的作用范围是整个新生代,没有被引用的对象会被回收,依然存活的对象会被送往Survivor区,如果Survivor区放不下会送往老年代。Survivor被分为了两个区,每次YGC都会把存活的对象复制到未被使用的那个区,然后将另一个区的空间完全清除,每次YGC两个Survivor都会互相交换。如果一个对象在Survivor区中反复被交换了14次,或者说经历了15次GC后(JVM默认值,可以配置但不能超过15)依然存活,会被放入老年代。老年代塞满后会触发FGC,FGC后依然无法存放对象会发生OOM(out of memory error)
- 方法区是堆的一个逻辑部分,但也有一个名字叫非堆,方法区是各个线程共享的内存区域,方法区也叫元空间。元空间使用的是计算机内存里面,用来存放常量池、方法元信息、类原信息。这个区域里面的数据不会被垃圾收集器回收,关闭JVM才会释放此区域所占用的内存
垃圾收集
Java对内存进行自动分配与回收管理,垃圾收集的目的是清除不再使用的对象,自动释放内存
GC Roots
Java采用GC Roots来判断对象是否存活、是否可以回收。如果一个对象与GC Roots之间没有直接或间接的引用关系,那么这个对象就是可以回收的。GC Roots包括了一下对象:
- 类静态属性中引用的对象
- 常量引用的对象
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
GC主要针对的是堆区,那么其他不需要垃圾回收的方法区、栈、本地方法栈中的对象及其引用的对象就是GC Roots
垃圾收集算法
- 引用计数法
维护一个计数器,一个对象有人引用就计数+1,没人引用就计数-1,当计数为0的时候就证明没人引用这个对象,会被视为垃圾。JVM的实现一般不采用这种方式:
- 每次对对象的赋值是都要维护引用计数器,且计数器本身也有一定的消耗
- 较难处理循环引用,有点像死锁。两个对象互相引用(这里有个问题,Spring怎么解决循环依赖的?)
- 复制算法
复制Eden和from(指的是有对象的Survivor)区到to(没有对象的Survivor)区,新生代多使用这种算法。好处是不会产生内存碎片,但是消耗空间(浪费了Survivor区一半的内存),并且这种算法建立在新生代对象存活率低的情况,如果新生代的对象存活率太高或者对象太大,复制会花费大量的时间
- 标记清除
用在老年代,先标记再清除。标记出要回收的对象,然后统一回收这些对象(两次扫描,相对耗时,有内存碎片)效率比较低,需要停止应用程序,JVM需要维持一个内存的空闲表,而且不连续的空间无法分配数组对象
- 标记压缩
这个算法为了解决内存碎片,采用了标记,清除,整理的策略。但是要花更多的时间,需要移动对象
JVM采用了分代收集的策略,在新生带采用复制算法(区域小、对象存活率低),在老年代(区域大、对象存活率高)采用标记清除和标记压缩相结合的方法(多次标记清除之后进行一次标记压缩)
垃圾收集器
- Serial
串行垃圾收集器,为单线程环境设计且只使用一个线程进行垃圾回收,回收时会暂停所有的用户线程(Stop the World,简称STW),主要应用于YGC。不适合服务器环境。它的实现有UseSerialGC、UseSerialOldGC,分别用来回收Young区和Old区
- Parallel
并行垃圾收集器,多个垃圾收集器并行工作,回收时用户线程也是暂停的,适用于科学计算、大数据处理首台处理等弱交互场景。主要实现有:ParNewGC(Serial收集器的多线程版本)、ParallelGC(并发收集器,吞吐量优先的收集器)、ParallelOldGC(Parallel收集器的老年代版本)
- CMS
并发垃圾收集器(ConcMarkSweep),用户线程和垃圾回收线程同时执行(并行或交替执行),不需要暂停用户线程,对响应时间有要求的场景多用这种垃圾收集器。通过初始标记、并发标记、重新标记、并发清除四个步骤完成垃圾回收工作,其中初始标记和重新标记会触发STW,其他阶段是并行的。CMS采用了标记清除算法,会产生空间碎片,可以通过配置-XX:UseCMSCOmpactAfFullCollection
来强制JVM在FGC完成后对老年代进行压缩,执行一次空间碎片整理,但压缩的过程也会出现STW,所以通常会通过配置XX:+CMSFullGCsBeforeCompaction=n
,在执行了n次(如果不配置默认是0)FGC后,JVM再对老年代进行空间整理
- G1
为了取代CMS收集器的服务端垃圾收集器,应用于多处理器和大容量内存中,充分利用了多CPU和多核的硬件优势,在实现高吞吐量的同时尽量满足垃圾收集暂停时间的要求,相对于CMS来说,G1收集器不会产生内存碎片,并且为停顿时间添加了预测机制。宏观上不再区分年轻代和老年代,把内存划分为多个(最大2048)同样大小(1-32M)的独立的子区域(Region),每个区域属于不同代,但不会固定的为某个代服务,然后并发的对其进行垃圾回收,收集过程是:初始标记、并发标记、最终标记、筛选回收。物理上不把内存划分为新生代和老年代,但逻辑上依旧保留了新生代和老年代,是一部分Region的集合,且不需要Region是连续的
总结一下一共有七大垃圾收集器
- SerialGC
- SerialOldGC
- ParNewGC
- ParallelGC
- ParallelOldGC
- ConcMarkSweepGC
- G1GC
JVM常用参数
JVM的参数有三类
- 标配参数
- X参数
-Xint 解释执行
-Xcomp第一次使用就编译成本地代码
-Xmixed混合模式,先编译后执行
- XX参数(重点关注对象)
- Boolean类型
+、-表示启用、关闭
- KV类型
查看参数
- 使用
jps
查看Java后台进程,获取进程编号
jinfo -flag [参数名] [进程号]
查看后台进程的各种信息
C:\Users\Administrator>jps
5904
6784 Jps
4636 HelloGC
7372 Launcher
C:\Users\Administrator>jinfo -flag PrintGCDetails 4636
-XX:-PrintGCDetails
我们也可以使用jinfo -flags [进程号]
来查看这个进程的所有参数
C:\Users\Administrator>jinfo -flags 4636
Attaching to process ID 4636, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.231-b11
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4273995776 -XX:MaxNewSize=1424490496 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:ThreadStackSize=256 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line: -XX:ThreadStackSize=256 -javaagent:C:\developerTools\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=55496:C:\developerTools\IntelliJ IDEA 2019.2.4\bin -Dfile.encoding=UTF-8
Non-default VM flags后面的参数是JVM所自带的
Command line后面的参数是我们自己添加的
- java -XX:+PrintFlagsInitial -version
打印JVM所有XX参数的默认值
- java -XX:+PrintFlagsFinal -version
打印JVM所有XX参数的实际值,包含了JVM和用户的修改,被修改的参数用:=
来标注
- java -XX:+PrintCommandLineFlags -version
打印所有被JVM或用户设置过的XX参数,就是PrintFlagsFinal里面被:=
标注的参数
常用参数
Heap
- -Xms1024k
-XX:InitialHeapSize=1024k
设置堆的初始大小1024k,堆默认大小是物理内存的1/64
- -Xmx1024k
-XX:MaxHeapSize=1024K
设置堆的最大值1024K,堆默认的最大大小是物理内存的1/4,我们通常会把堆的最大值和初始值设置成一致的,防止堆不断地扩容与回缩,影响性能
- -Xmn1024k
设置新生代的初始及最大大小为1024K,也可以用-XX:MaxNewSize
和-XX:NewSize
来细化设置
- -XX:OldSize=1024
设置老年代大小为1024K
- -XX:SurvivorRatio=5
设置Eden区在新生区所占比例大小,设置成5那么新生区的比例就是5/1/1,但这一类的参数权重要比显式设置大小的要低
- -XX:NewRatio=4
设置老年代在整个堆区所占比例大小,4就是老年代占4份,新生代占1份
- -XX:MaxTenuringThreshold=12
设置对象在新生区最大存活次数为12次
- -XX:MetaspaceSize=1024K
设置元空间的大小为1024K
Stack
- -Xss256K
-XX:ThreadStackSize=256K
设置单个线程栈的大小为256K ,默认为512k-1024k,跟平台和JDK有关。显示0代表使用系统默认值
GC
- -XX:+PrintGCDetails
启用打印GC日志
JVM中垃圾收集器是成对存在的,分别对应Young区和Old区,通常配置一个另一个会跟随启动,但是如果配置了非成对的垃圾收集器,会报错:无法创建虚拟机
参数 |
新生代垃圾收集器 |
新生代算法 |
老年代垃圾收集器 |
老年代算法 |
-XX:+UseSerialGC |
SerialGC |
复制 |
SerialOldGC |
标记整理 |
-XX:+UseParNewGC |
ParNew |
复制 |
SerialOldGC |
标记整理 |
-XX:+UseParallecGC 或-XX:+UseParallecOldGC |
Parallel |
复制 |
ParallelOld |
标记整理 |
-XX:+UseConcMarkSweepGC |
ParNew |
复制 |
CMS+SerialOld(备用) |
标记清除 |
-XX:+UseG1GC |
G1整体上采用了标记整理算法,局部采用复制算法 |
|
|
|
- -XX:MaxGCPauseMillis=n
设置G1收集器的最大GC停顿时间。软目标,JVM尽可能(不保证)将停顿小于这个时间
常见的OOMError
- java.lang.StackOverflowError
栈溢出,递归就很容易出现这个问题
- java.lang.OutOfMemoryError:Java heap space
堆溢出,老年代满了,大对象或者巨多的对象,堆空间存放不下
- java.lang.OutOfMemoryError:GC overhead limit exceeded
GC回收时间过长,超过98%的时间用来做GC并且只回收了2%不到的内存,连续多次GC但都只回收了不到2%的内存。这时JVM会放弃GC直接抛出错误
- java.lang.OutOfMemoryError:Direct buffer memory
由NIO引起的本地内存超出。NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,在一些特殊场景可以提高性能,避免了在Java堆和Native堆中来回复制数据
- ByteBuffer.allocate(capability):分配JVM堆内存,GC可以回收。但需要把对象拷贝到JVM堆区,速度慢
- ByteBuffer.allocateDirect(capability):分配本地内存,不属于GC管辖范围,但不需要拷贝,速度相对快
如果不断分配本地内存,堆内存很少使用,JVM不会执行GC,DirectByteBuffer对象不会被回收,可能本地内存会在JVM堆内存之前占满,这时候再分配本地内存的时候就会报错
- java.lang.OutOfMemoryError:unable to create new native thread
高请求并发服务器线程数超过系统最大数,与平台有关,Linux默认最大线程是1024
- java.lang.OutOfMemoryError:Metaspace
元空间溢出,加载的类太多,或者是常量池、静态变量太多