Java内存区域与内存溢出异常
文章目录
1.JVM内存基本结构
注:本图来源于深入理解Java虚拟机-----周志明 著
程序计数器
是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,Java虚拟机中的多线程是通过轮流切换并分配处理器所执行的时间的方式来实现的,为了线程切换后恢复到正确的执行位置,每一个线程都有一个独立的,互不影响的程序计数器。程序计数器是唯一一个在Java虚拟机规范没有规定OutOfMemoryError情况的区域。
Java虚拟机栈
与程序计数器一样,虚拟机栈也是线程私有的,每个方法执行时都会创建一个栈帧,用于储存局部变量表,动态链接,方法出口地址,操作数栈等信息,每一个方法从调用到执行完成的过程,就对应着一个栈帧从Java虚拟机栈中入栈到出栈的过程。
局部变量表
局部变量表可以存放8大基本类型,也可以存放引用类型
本地方法栈
和虚拟机栈有点相似,Java虚拟机栈用来存储Java方法,即字节码服务,而本地方法栈用来存储要调用的Native方法。
Java堆
这是JVM中比重比较大的一块区域,GC的操作也是在这段区域进行,该区域用来存储类实例的实际空间,堆区域是线程公有的一块区域。堆在物理角度可以存储在不连续的内存中,只要逻辑上连续即可,因为垃圾回收机制用的是一种分代回收的方式,所以对空也有对应的内存划分,Java堆可以分为新生代和老年代,再细致一点有Eden空间、From Survuvor空间、To Survivor空间,默认为8:1:1。垃圾回收机制这里我们不细说,会在后续的博文中详细说明。
方法区
方法区和堆空间一样是线程共有的区域,它用于存储类信息、常量、静态变量,Java虚拟机将方法区描述为堆的一个逻辑部分。但是它还有一个别名Non-Heap(非堆),目的是和堆区域分开。
对于HotSpot虚拟机,很多人愿意把方法区叫做永久代,但是两者其实并不等价,会被称作永久代的原因仅仅是HotSpot虚拟机的设计者将GC的分代回收扩展到了方法区,或者说仅仅是用永久代来管理方法区而已,这样垃圾回收器就可以像管理堆一样来管理方法区,这样就省去了专门为方法区编写管理内存的代码。但是这种处理方式有很多的问题,像内存泄漏等等。所以在JDK 1.8版本,方法区的管理抛弃了永久代这种方法,改用元空间 具体什么是元空间会在下面的内容专门讲述。
运行时常量池
运行时常量池是方法区的一部分,编译阶段产生的字面量和符号引用都存储在运行时常量池中。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java规范中规定的内存区域。通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
2.对象的创建
一个new关键字要执行三个步骤:
- 为该对象申请空间
- 对实例变量附上初始值
- 将该对象的索引返回
申请空间的过程
虚拟机遇到一条存在new关键字的指令时,会检查这个指令的参数能否在常量池中定位一个类的符号引用,并检查这个类是否进行了加载、连接、初始化。如果没有,那就进行类的加载过程。类的加载检查通过后,才会对其分配空间。
分配空间有两种方式一种叫指针碰撞,一种叫空闲列表
指针碰撞
堆空间中已经分配的内存放在一边,未分配的放在另一边,它们中间有一个指针来区分已分配和未分配空间,当要分配一个空间时。这个指针就会忘未分配的空间移动该对象分配空间的大小。指针碰撞只有在内存比较规整的情况下才可以使用,如果已经使用的内存和未分配的内存相互交错,就没有办法使用指针碰撞的方式了。
空闲列表
虚拟机会去维护一张列表,这张列表回去记录堆空间中有哪些空间已经使用,哪些空间未被使用。在分配时会选择一块足够大的空间进行分配,更改这张表中的数据。
选择哪种分配方式基于Java堆空间内存是否规整,而堆空间是否规整取决于虚拟机所采用的垃圾回收器是否带有压缩整理的功能。
对象的内存布局这块简单说一下,并不详细的说明
一个对象在内存中的布局可以分为3个区域:对象头、实例数据、对齐填充
对象头:包含该对象的哈希码等信息
实例数据:该对象实际储存的数据
对齐填充:这部分不是必然存在的,没什么特别的意义,仅仅起到占位符的作用,由于HotSpot虚拟机的自动内存管理要求对象起始地址必须是8字节的整数倍,这也就要求了每个对象的空间大小必须是8字节的整数倍,否则就会产生一些碎片空间,为了弥补这些碎片,我们使用对齐填充对其进行填充。
3.对象的访问方式
- 通过句柄访问对象
- 通过直接指针来访问对像
对象类型数据也可以叫做元数据,属于方法区,元数据保存着该对象所对应的类的信息
两种访问各有优势,通过句柄访问的最大好处是,当对象被移动(垃圾收集时对象移动是非常普遍的行为)时,只用更改句柄池中的指针即可,不用更改reference的值
使用直接指针访问的好处是速度更快,节省了一次指针定位的开销,HotSpot虚拟机采用的就是这种方式。
4Java堆内存溢出例子
我们现在的电脑内存都很大,想让堆溢出就要通过更改JVM的相关参数,减少堆的大小。
将堆的空间更改为5M,然后运行下列程序
package com.mec.memory;
import java.util.ArrayList;
public class MyTest1 {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<Object>();
while (true) {
list.add(new MyTest1());
}
}
}
运行结果:
最终出现了OutOfMemoryError 内存溢出错误
发生了Java堆溢出
我们用 jvirtualvm 做一个监控
为了方便大家的学习我们先介绍几个JVM的参数:
参数 | 作用 |
---|---|
-Xmx | JVM中堆内存的最大值,比如-Xmx5M,将最大值设为5M |
-Xms | 启动时JVM堆内存的最小值,比如-Xms5M,将最小值设为5M |
-XX:+HeapDumpOnOutOfMemoryError | 当出现堆内存溢出的错误时,会生成堆的一个快照文件,可供jvirtualvm分析使用 |
-XX:HeapDumpPath | 这个参数一般与 -XX:+HeapDumpOnOutOfMemoryError连用。用来指定快照文件的保存路径,也可以不写,默认路径为和src平级 |
我们配置好这些参数
打开jvirtualvm装入堆的快照文件
我们会发现这个Mytest1对象的数量达到了24万个,占用了整个堆的大约97%,所以发生了OutOfMemoryError错误
垃圾收集器,一般是不用程序员主动调用的,它会在规定的时间间隔后自动的调用,如果我们显示的主动调用GC还会发生内存溢出吗?
我们对上述程序进行改动
package com.mec.memory;
import java.util.ArrayList;
public class MyTest1 {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<Object>();
while (true) {
list.add(new MyTest1());
System.gc();
}
}
}
结果虽然没有发生堆内存溢出
但是垃圾回收活动进行的很频繁
5.Java虚拟机栈
在正式开讲前,我们了解几个HotSpot虚拟机参数:
参数 | 作用 |
---|---|
-Xoss | 设置本地方法栈的大小 ,HotSpot虚拟机将虚拟机栈、本地方法栈合并了,所以如果你使用的是HotSpot虚拟机,这个参数虽然存在,但是是无用的 |
-Xss | 整改虚拟机栈的大小,最小160K |
虚拟机栈存的是栈帧,而栈帧又与方法执行有关,我们将虚拟机栈的大小设置成160K,执行下面的无限递归方法。
package com.mec.memory;
public class MyTest3 {
private int count = 0;
public void test() {
try {
count++;
test();
} catch(Throwable e) {
System.out.println(count);
throw e;
}
}
public static void main(String[] args) {
MyTest3 test3 = new MyTest3();
test3.test();
}
}
运行结果
最终造成了StackOverFlowError错误
6.死锁的例子
死锁的定义
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
==注:==该定义来源于百度百科
package com.mec.memory;
public class MyTest2 {
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args){
Thread a = new Thread(new Obj1(), "Thread_1");
Thread b = new Thread(new Obj2(), "Thread_2");
a.start();
b.start();
}
}
class Obj1 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock1 running");
synchronized(MyTest2.lock1){
System.out.println("Lock1 lock obj1");
Thread.sleep(3000);
synchronized(MyTest2.lock2){
System.out.println("Lock1 lock obj2");
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
class Obj2 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock2 running");
synchronized(MyTest2.lock2){
System.out.println("Lock2 lock obj2");
Thread.sleep(3000);
synchronized(MyTest2 .lock1){
System.out.println("Lock2 lock obj1");
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
}
我先说明一下上述程序为什么会产生死锁:
主函数将类lock1和类lock2中的两个run方法都跑了起来,这两个线程都跑起来后分别进入了lock1和lock2的对象锁,然后程序就进入了一个僵局。
这时候lock1和咯lock2都是锁住的状态。两个线程无法都无法跑下去了。
我们使用jvirtualvm来检测一下
这个工具真的很强大,可以直接帮你检查出死锁并且能看到比较具体的内容
7.元空间
7.1元空间的概念
我们知道在JDK1.8之后永久代这个概念就消失了,永久代被元空间所替代,为什么永久代消失了,具体的原因和元空间的内容,大家可以阅读以下这篇文章。写的非常的不错,请大家先看下这篇文章再看博文中下面的内容
永久代为什么消失了------点击此处阅读文章
关于元空间溢出的一个例子
元空间默认的大小为21M, 并且会根据使用情况进行扩容,元空间使用的是本地内存,也就是说可以扩容到你本地内存的大小。所以你不设置元空间最大空间时,很难看到元空间溢出的情况。
我们先了解一下更改元空间大小的虚拟机参数
参数 | 作用 |
---|---|
-XX:MaxMetaspaceSize | 更改最大的元空间大小,比如-XX:MaxMetaspaceSize =10m |
我们知道元空间存储的是对象所对应的类的信息,包括常量池的内容、静态成员等等,为了让元空间溢出,我们就要在程序性运行中不停的动态产生类,从而导致元空间OutOfMemory
package com.mec.memory;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class MyTest4 {
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyTest4.class);
enhancer.setUseCache(false);//不使用缓冲区。没有这一行就算你改了参数也不可能溢出
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
System.out.println("执行了");
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
}
运行结果
Metaspace OutOfMemoryError
我们用jvirtualvm去监控一下,为了方便监控我们将最大元空间大小改为150M。
运行程序我们发现加载的类再不停的产生,因为我们的程序不停的动态产生新的类
最终元空间的空间被使用殆尽到达了150m,元空间内存溢出了
8.获得JVM信息的工具
除了jvirtualvm这样的可视化的工具以外,还有很多命令行的工具
8.1 jmap
我们先看一下jmap都有哪些功能``
C:\Users\hp>jmap
Usage: //使用方法
jmap [option] <pid>
(to connect to running process)
jmap [option] <executable <core>
(to connect to a core file)
jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
<none> to print same info as Solaris pmap
-heap to print java heap summary//打印堆xinxi
-histo[:live] to print histogram of java object heap; if the "live"
suboption is specified, only count live objects
-clstats to print class loader statistics //打印类加载器信息
-finalizerinfo to print information on objects awaiting finalization
-dump:<dump-options> to dump java heap in hprof binary format
dump-options:
live dump only live objects; if not specified,
all objects in the heap are dumped.
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> to pass <flag> directly to the runtime system
这里面又很多选择,我们试一下 -clstats
jmap -clstats PID
PID就是一个进程号,但是windows系统并没有ps -ef|grep 这样的指令,我们可以用tasklist指令来获得所有进程的版本号
这个javax.exe就是我们运行的程序,它对应的进程号为4320
执行结果
通过上图的这些信息我们可以知道:
class_loader(类加器),
classes(加载的类的数量)
bytes(所占用的字节)
parent_loader(父类加载器)
live(类加载是否存活)
type(类加载器的类型)
我们再试一下 -heap可以得到以下信息
C:\Users\hp>jmap -heap 5180
Attaching to process ID 5180, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration://堆内存的一些参数
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 1052770304 (1004.0MB)
NewSize = 22020096 (21.0MB)
MaxNewSize = 350748672 (334.5MB)
OldSize = 45088768 (43.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation //年轻代,再细分的话,会分为以下三部分内容
Eden Space:
capacity = 16252928 (15.5MB) //容量
used = 16252848 (15.499923706054688MB)//已使用的空间
free = 80 (7.62939453125E-5MB)//剩余空间
99.99950778099799% used
From Space:
capacity = 524288 (0.5MB)
used = 0 (0.0MB)
free = 524288 (0.5MB)
0.0% used
To Space:
capacity = 524288 (0.5MB)
used = 0 (0.0MB)
free = 524288 (0.5MB)
0.0% used
PS Old Generation//老年代
capacity = 45088768 (43.0MB)
used = 647280 (0.6172943115234375MB)
free = 44441488 (42.38270568847656MB)
1.4355681663335755% used
8.2 jstat
先了解一下它的功能:
C:\Users\hp>jstat
invalid argument count
Usage: jstat -help|-options
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
Definitions:
<option> An option reported by the -options option
<vmid> Virtual Machine Identifier. A vmid takes the following form:
<lvmid>[@<hostname>[:<port>]]
Where <lvmid> is the local vm identifier for the target
Java virtual machine, typically a process id; <hostname> is
the name of the host running the target Java virtual machine;
and <port> is the port number for the rmiregistry on the
target host. See the jvmstat documentation for a more complete
description of the Virtual Machine Identifier.
<lines> Number of samples between header lines.
<interval> Sampling interval. The following forms are allowed:
<n>["ms"|"s"]
Where <n> is an integer and the suffix specifies the units as
milliseconds("ms") or seconds("s"). The default units are "ms".
<count> Number of samples to take before terminating.
-J<flag> Pass <flag> directly to the runtime system.
我们重点关注一下MC 和MU这两个参数
MC 是元空间的大小,MU是已使用的元空间的大小
8.3 jcmd
jcmd (从JDK 1. 7开始增加的命令)
- jcmd pid VM. flags: 查看JVM的启动参数
- jcmd pid help: 列出当前运行的Java进程可以执行的操作
- jcmd pid help JFR.dump:查看具体命令的选项
- jcmd pid PerfCounter.print:查看JVM性能相关的参数
- jcmd pid VM. uptime:查看JVM的启动时长
- jcmd pid cc.class_ histogram: 查看系统中类的统计信息
- jcmd pid Thread.print:查看线程堆栈信息
- jcmd pid GC. heap_ dump filename: 导出Heap_ dump文件,导出的文件可以通过jvisualvm查看
- jcmd pid VM. system properties:查看JVM的属性信息
这里面的pid就是进程号,我们知道通过tasklist可以看所有的进程号,但是这样真的很麻烦,因为要从所有的进程号中,找到我们需要的哪一个,所以这次我们换一个简单的命令——jps
如果用jps -l的话能看到比较详细的内容,除此之外还有其他的一些选择
那个数字就是对应的进程号
关于jcmd的语句上面说了一部分,我不可能一一展示给大家看,大家自己试试吧。
查看JVM的启动参数
列出当前java线程可以进行的操作
还有很多我就不试了。
8.4小总结
对于上述的各种JVM的分析工具还有很多,比如jhat、jmc等等,这里就不一 一介绍了,本人并不认为我一口一气学完所有的工具,或者我一口气学完一个类中的所有方法是一种很好的学习方式,因为当下并没有让你应用的应用场景,总而言之,你真正会用的招式才是你的,你模仿别人的招式并不是你的。