JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。
程序计数器(PC寄存器):
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,
因此,为了能够使得每个线程都在线程切换后能够恢复在切 换 之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,
否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,
因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
Java虚拟机栈(Java Virtual Machine Stacks):
Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)
指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)
方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
本地方法栈:
与上面不同的是Java栈是给Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
堆:
Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。
方法区:
与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
堆空间结构图:
(注意:在JDK1.8版本废弃了永久代)
整个堆空间分成两半:
伊甸园:一开始加载或者创建新对象都存到伊甸园中。
S0和S1:存放GC清理后,幸存下来的对象。
老年代:经过几轮清理存下来的对象,就会放到老年代中。
垃圾回收(GC,Garbage Collection),垃圾回收器(GC, Garbage Collector)。
组合方式就是:年轻代和老年代的组合方式。
垃圾回收器几种组合方式:
其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案
(红色虚线)在jdk8时将这两个组合声明为废弃,并在jdk9中完全取消
(绿色虚线)在jdk14中废弃
(绿色虚线)jdk14中,删除CMS垃圾收集器
Serial GC:
新生代的垃圾回收器,收集工作是单线程的,基于复制算法的。
Paralel Scavenge GC:
新生代的垃圾回收器,并行收集的多线程收集器,采用的是复制算法。
ParNew GC:(默认的新生地啊垃圾回收器)
新生代的垃圾回收器,Serial 收集器的多线程版本,采用的也是复制算法。
可通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。Serial Old GC:
老年代的Serial 垃圾收集器,单线程串行的收集器,是基于标记-整理算法。
Parallel Old GC:
老年代的垃圾收集器,多线程并发的收集器,基于标记整理算法。
CMS GC:
老年代的垃圾收集器,多线程并发的收集器,基于标记清除算法。
G1 GC:
青年代和老年代的垃圾收集器,多线程并发,并行的收集器,基于复制算法,标记-整理 算法。
标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。
标记-清除 缺点:
Java堆中新生代的垃圾回收算法。
复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉。
特点:
Java堆中老年代的垃圾回收算法。
标记-压缩 算法:就是先将标记清除的对象清除,之后再将其他存活的对象整理压缩到内存的一端,此时可以直接清除边界处的内存。
特点:
一般虚拟机可都是采用分代收集算法:
新生代一般采用:复制算法。
老年代一般采用:标记-整理算法。
整个垃圾回收机制的过程:
新生成的对象首先放到年轻代Eden(伊甸园)区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。
jdk命令:jstat [-options参数] 进程号 [多少毫秒显示一次]
Options 参数如下:
-gc:统计 jdk gc时 heap信息,以使用空间字节数表示
-gcutil:统计 gc时, heap情况,以使用空间的百分比表示
-class:统计 class loader行为信息
-compile:统计编译行为信息
-gccapacity:统计不同 generations(新生代,老年代,持久代)的 heap容量情况
-gccause:统计引起 gc的事件
-gcnew:统计 gc时,新生代的情况
-gcnewcapacity:统计 gc时,新生代 heap容量
-gcold:统计 gc时,老年代的情况
-gcoldcapacity:统计 gc时,老年代 heap容量
-gcpermcapacity:统计 gc时, permanent区 heap容量
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
单位:KB
演示一个堆溢出的效果:
package com.itholmes;
import java.util.ArrayList;
public class MyGC {
byte[] b = new byte[1024*1024*2];//相当于一个对象2M大小
public static void main(String[] args) throws Exception{
ArrayList<Object> list = new ArrayList<>();
//睡眠20s
Thread.sleep(20000);
for(int i = 0;;i++) {
Thread.sleep(20);
list.add(new MyGC());
System.out.println(i);
if(i % 20 == 0) {
list = null;
list = new ArrayList<>();
}
}
}
}
我们在对象中,创建一个2兆大小的数组,这样一个对象大约就占2M。不断的向一个list中添加,并且到一定时刻清空list(设置list为null,触发垃圾回收机制)。
设置堆结构大小:
右键 -》 run as -》 run configurations如下图:
(在eclipse当中,设置命令参数;正常代码执行,也可以直接通过命令java -Xmx36m等等,这种方式来执行。)
-Xmx:最大堆大小。-Xmx36m就是最大堆36M。
-Xms:初始堆大小。-Xms16m就是最小堆16M
这样堆的大小就会在最大堆和最小堆之间来回变化,有一定的延展性。上面是我们写死了,最大最小都是36M方便测试。
-Xmn:年轻代大小。-Xmn16m就是年轻代16M
-XX:SurvivorRatio:eden(伊甸园) :(S0+S1) 的比例。S0和S1的大小相同。例如:年轻代大小16M,如果 -XX:SurvivorRatio=2 那么伊甸园:S0:S1 就是 8:4:4 的比例。
按照这个逻辑推算,那么上面的代码执行就会造成堆溢出的效果。
jconsole 查看java进程的一个可视化工具。
我们新建连接,选择对应java进程后,就能看到相关信息:
与上面的jvm 参数命令配置,一个设置一个查看:
很多人认为这个命令是linux系统自带的,其实是jvm命令,查看当前java进程。
和jconsole命令差不多,比jconsole显示的命令更加详细一些,还可以远程连接。
对于tomcat想要远程监控,操作如下:
配置bin目录下的catalina.sh/bat文件:
jinfo [ option ] pid
查看某个java进程的详细信息。
jmap [options] pid
jstack [ option ] vmid