JVM内存结构之堆、栈、方法区以及直接内存、堆和栈区别
一、 理解JVM中堆与栈以及方法区
堆(heap):FIFO(队列优先,先进先出);二级缓存;*JVM中只有一个堆区被所有线程所共享;对象和数组储存在里面;调用对象速度较慢;生命周期由虚拟机JVM的垃圾回收机制GC制定;由JVM动态分配空间;堆内存用来存放由new创建的对象和数组。
在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
栈(stack):FILO 或者叫LIFO(线性表,后进先出);一级缓存;每个线程都会有一个独立的栈空间,所以线程之间是不共享数据的;对象的引用变量以及基本数据类型存储在里面;速度较快;生命周期:当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用;缺乏灵活性。
方法区:方法、静态变量(static)、常量池;方法区又称为永久区或者代码区,存放的是一些固定不变的代码 方法,静态的,常量池
二、 堆和栈的优缺点
1、 大家都知道java也叫做“C++-”;堆和栈对C++而言是需要程序员去直接管理堆和栈的,而JAVA是自动管理堆和栈的,栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。
2、 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器GC会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢;栈有一个很重要的特殊性,就是存在栈中的数据可以共享。
a、 最主要的区别就是栈内存用来存储局部变量和方法调用。而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
b、 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
c、 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.*Error。
而如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。
d、 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满。如果递归没有及时跳出,很可能发生*Error问题。
你可以通过-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值。(关于-Xss与-Xms以及-Xmx是不是很眼熟?我看到的时候马上想到了eclipse.ini下面设置,有想了解的这里:https://www.jb51.net/article/126323.htm)
这就是Java中堆和栈的区别。理解好这个问题的话,可以对你解决开发中的问题,分析堆内存和栈内存使用,甚至性能调优都有帮助。
三、 在代码编译与运行中去理解
Java Heap Memory
(对象级)
堆内存(heap memory)是被用来在runtime的时候给对象和jre的那些class分配内存的。注意是runtime的时候。不管你何时创建对象,创建任何一个对象,这些对象都是被创建在了heap空间里的。那个我们熟悉的gc(垃圾回收站)负责把那些不再被引用(reference)的对象从heap memory中清理掉,这也是gc的职责所在。在heap空间里创建的任何对象都是全局访问的。可以被应用程序的任何地方引用。
Java Stack Memory
(线程级)
java里的stack内存(stack memory)是被用来线程的执行的。也就是stack是线程级别的。而heap是对象级别的。这个stack里边包含了方法里边那些定义的值,这些值随着一次方法执行完毕后就消失了;还包含了引用地址。这个引用地址就是对存放在heap memory中的一个链接。你可以理解为关系数据库里边的外键,nosql中的外链。总之你理解就行。stack memory由于她是个stack结构。所以呢,他也遵循LIFO,就是后进先出的顺序。一个方法不论什么时候被调用,一个针对该方法的全新的block就会在stack memory里被创建,用来存储这个方法里的本地基本类型的值以及这个方法对其他对象的引用地址。一旦方法执行结束,这个block的状态就变为unused了,就是变为了空闲状态,就变为available了。宣布单身了,等着下一个method来用她。stack memory的size相比heap memory的size要小得多。
现在就让我们上一个simple program来更好的理解一下堆栈memory。
Memory.java
public class Memory {
public static void main(String[] args) { // Line1
int i=1; // Line 2
Object obj = new Object(); // Line 3
Memory mem = new Memory(); // Line 4
mem.foo(obj); // Line 5
} // Line 9
private void foo(Object param) { // Line 6
String str = param.toString(); //// Line 7
System.out.println(str);
} // Line 8
}
下面这个图就展示了在上面这个程序中的stack和heap memory的存储和引用关系。堆栈怎么被用来存储基本类型值(primitive value)以及对象以及对象的引用。
接下来我们就一步步的来看上面的那个program的执行情况。
- 一旦我们运行了这个程序,它就会把所有的runtime class load 到 heap空间。当main()方法在line1那个地方被发现后,Java Runtime就会创建stack memory给main()方法这个线程来用。
- 在line2那个地方,我们创建了一个primitive(基本类型)的局部变量,这个变量自然是被存储到了main()的stack memory里的。
- 在line3那个位置,我们创建了一个对象,按照前面说的,这个对象自然是存储在heap memory里边的,并且在stack memory里边也有个这个对象的引用地址被存储了进去。line4的对象创建过程和line3是一样的。
- 现在我们来到了line5这个地方,这一行我们调用了foo()方法,这时候一个block在stack的顶部(为什么是顶部了?因为栈是队列,先进先出)被创建,这个block现在专门为foo()方法服务。由于java是按值传递,所以在line6那个位置一个新的对象引用就会在foo() 方法的stack block中被创建。
- 在line7那个位置,一个字符串被创建,这个串是在heap空间的string池(String Pool)中。并且对这个string对象的引用自然也在foo()方法的stack空间里被创建了。
- 在line8那个地方foo()方法就被终止了,在方法结束的时候,在stack中为foo()分配的那个block重新变回空窗期,宣布available了。
- 在line9那个地方,main()方法也要结束了。自然为main()创建的stack memory就会被destory掉了。自此,Java Runtime 释放所有的memory然后结束程序的执行!
对以上内容的原理解释(并没有深入):
JVM中的堆和栈
JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。 我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.
从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。
每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程 共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。
Java 把内存划分成两种:一种是栈内存,另一种是堆内存。在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。
四、 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
关于NIO这里大佬博客:https://www.cnblogs.com/geason/p/5774096.html
我会在后续借鉴与总结
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常
直接内存(堆外内存)与堆内存比较
package com.xnccs.cn.share;
import java.nio.ByteBuffer;
/**
* 直接内存 与 堆内存的比较
*/
public class ByteBufferCompare {
public static void main(String[] args) {
allocateCompare(); //分配比较
operateCompare(); //读写比较
}
/**
* 直接内存 和 堆内存的 分配空间比较
*
* 结论: 在数据量提升时,直接内存相比非直接内的申请,有很严重的性能问题
*
*/
public static void allocateCompare(){
int time = 10000000; //操作次数
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocate(int capacity) 分配一个新的字节缓冲区。
ByteBuffer buffer = ByteBuffer.allocate(2); //非直接内存分配申请
}
long et = System.currentTimeMillis();
System.out.println("在进行"+time+"次分配操作时,堆内存 分配耗时:" + (et-st) +"ms" );
long st_heap = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocateDirect(int capacity) 分配新的直接字节缓冲区。
ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
}
long et_direct = System.currentTimeMillis();
System.out.println("在进行"+time+"次分配操作时,直接内存 分配耗时:" + (et_direct-st_heap) +"ms" );
}
/**
* 直接内存 和 堆内存的 读写性能比较
*
* 结论:直接内存在直接的IO 操作上,在频繁的读写时 会有显著的性能提升
*
*/
public static void operateCompare(){
int time = 1000000000;
ByteBuffer buffer = ByteBuffer.allocate(2*time);
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用来写入 char 值的相对 put 方法
buffer.putChar('a');
}
buffer.flip();
for (int i = 0; i < time; i++) {
buffer.getChar();
}
long et = System.currentTimeMillis();
System.out.println("在进行"+time+"次读写操作时,非直接内存读写耗时:" + (et-st) +"ms");
ByteBuffer buffer_d = ByteBuffer.allocateDirect(2*time);
long st_direct = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用来写入 char 值的相对 put 方法
buffer_d.putChar('a');
}
buffer_d.flip();
for (int i = 0; i < time; i++) {
buffer_d.getChar();
}
long et_direct = System.currentTimeMillis();
System.out.println("在进行"+time+"次读写操作时,直接内存读写耗时:" + (et_direct - st_direct) +"ms");
}
}
输出:
在进行10000000次分配操作时,堆内存 分配耗时:12ms
在进行10000000次分配操作时,直接内存 分配耗时:8233ms
在进行1000000000次读写操作时,非直接内存读写耗时:4055ms
在进行1000000000次读写操作时,直接内存读写耗时:745ms
可以自己设置不同的time 值进行比较
分析
从数据流的角度,来看
非直接内存作用链:
本地IO –>直接内存–>非直接内存–>直接内存–>本地IO
直接内存作用链:
本地IO–>直接内存–>本地IO
直接内存使用场景:
有很大的数据需要存储,它的生命周期很长
适合频繁的IO操作,例如网络并发场景
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
***************************************************************以上内容经我学习了大佬的博客后总结和搬运的:
---------------------
原文:
https://blog.csdn.net/qq_41675686/article/details/80400775
或上海尚学堂java培训整理编辑
https://mp.weixin.qq.com/s/lVgrYh2jRBqUnSLnjUaAjg?
https://blog.csdn.net/qq_31997407/article/details/79675371