Java内存的详细分析(包括垃圾回收)
程序员文章站
2022-07-15 15:05:59
...
转自:http://kakajw.iteye.com/blog/933266
1.JAVA 的内存概述:
JVM系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。当然,从进程是操作系统资源分配的单位这个角度来看,每个主内存对应于一个进程,多个线程共享该进程的资源(主内存)。
每条线程(主要处理用户定义的运算)都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。他们从主内存中取数据, 然后计算, 再存入主内存中。
当多条线程同时对主存的同一临界资源操作时,就会有线程同步问题;一些是JVM同步机制的大致过程:
(1) 获取对象监视器的锁(lock)
(2) 清空工作内存数据, 从主存复制变量到当前工作内存, 即同步数据 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 将工作内存数据刷回主存 (store and write)
(5) 释放对象监视器的锁 (unlock)
注意: 其中4,5两步是同时进行的.
这些涉及到线程同步问题,在这里就不累述了,简单了解一下。
2. 从进程和线程的角度认识堆和栈
Heap Memory(堆内存):虚拟机的堆内存保存的是对象,类变量以及实例变量,它被所有线程共享,常说的垃圾回收就是对堆内存的回收。Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。也就是上述的主内存(Java Heap Memory)。
Stack Memory 栈内存:虚拟机的每一个线程都有一个私有的栈,当一个方法被调用时,下面内容被作为一个Frame (帧)被创建并且被压入栈中:
+ 局部变量:包括基本数据类型,对象的引用和返回值地址。
+ 一个自己的操作栈:帧内局部变量进行运算时使用,也用于传递方法的参数和接受方法的返回值。
+ 一个当前方法所在类的Runtime constant pool (常量池)的引用。
方法调用完成时,帧出栈,并销毁,无论方法是正常结束还是有未捕获的异常。
Method Area 方法区(或者代码区): 当JVM加载一个class时 ,将该类的一些信息保存到Method Area,包括Runtime constant pool ,方法数据,方法和构造器代码,域等。Runtime constant pool 则 包括类名,父类名,静态变量等。Method Area在逻辑上属于Heap(因为和堆一样是被线程共享的,属于主内存)。不过它垃圾回收与Heap可能不同,取决于JVM的实现。
当通过new Class()方式创建一个实例时,JVM在Method Area寻址到该类的基本信息, 同时进行相关实例的初始化(包括实例变量),存贮在Heap中。
3. 堆和栈的进一步认识
下面我们从JVM的内存管理原理的角度来深入认识堆(Stack)和栈(Heap),并通过这些原理认清Java中静态方法和静态属性的问题。
Stack(栈)是JVM的内存指令区。Stack管理很简单,push一定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,并且每次操作的数据或者指令字节长度是已知的。所以Java 基本数据类型,Java 指令代码,常量都保存在Stack中。
Java栈是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。*Error:如果在线程执行的过程中,栈空间不够用,那么JVM就会抛出此异常,这种情况一般是死递归造成的。
Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
由于Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是随机分配内存,不定长度,存在内存分配和回收的问题;因此在JVM中另有一个GC进程,定期扫描Heap ,它根据Stack中保存的4字节对象地址扫描Heap ,定位Heap 中这些对象,进行一些优化(例如合并空闲内存块什么的),并且假设Heap 中没有扫描到的区域都是空闲的,统统refresh(实际上是把Stack中丢失了对象地址的无用对象清除了),这就是垃圾收集的过程。
4.堆栈分离的好处
从JAVA内存模型的角度去理解面向对象的设计,我们就会发现对象它完美的表示了堆和栈,对象的数据放在堆中,而我们编写的那些方法一般都是运行在栈中,因此面向对象的设计是一种非常完美的设计方式,它完美的统一了数据存储。
JVM的体系结构
我们首先要搞清楚的是什么是数据以及什么是指令。然后要搞清楚对象的方法和对象的属性分别保存在哪里。
1)方法本身是指令的操作码部分,保存在Stack中;
2)方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操作码和指令操作数构成了完整的Java 指令。
3)对象实例包括其属性值作为数据,保存在数据区Heap 中。
非静态的对象属性作为对象实例的一部分保存在Heap 中,而对象实例必须通过Stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在Stack中的地址指针。
非静态方法和静态方法的区别:
非静态方法有一个和静态方法很重大的不同:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,这个隐含的参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap 中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。
静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。当然此时静态方法是存取不到Heap 中的对象属性的。
总结一下该过程:当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。然后程序计数器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap数据区的;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到Heap 数据区了。
静态属性和动态属性:
前面提到对象实例以及动态属性都是保存在Heap 中的,而Heap 必须通过Stack中的地址指针才能够被指令(类的方法)访问到。因此可以推断出:静态属性是保存在Stack中的,而不同于动态属性保存在Heap 中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,也因此不管什么指令(类的方法),都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。
5. 再说说堆(Heap)的内部结构
前面谈到堆和垃圾回收,这里作进一步分析。
JVM堆一般又可以分为以下三部分:
Java Heap分为3个区,Young,Old和Permanent。Young(年轻代)保存刚实例化的对象。当该区被填满时,GC会将对象移到Old(年老代)。Permanent(永久代)主要是存储的是java的类信息,包括解析得到的方法、属性、字段等等。JVM的Heap分配可以使用-X参数设定,
-Xms
初始Heap大小
-Xmx
java heap最大值
-Xmn
young generation的heap大小
Older区的大小等于-Xmx减去-Xmn,不能将-Xms的值设的过大,因为第二个线程*运行会降低JVM的性能。
对照图,我们再详细了解一下。
◆ Perm
Perm代主要保存class,method,filed对象,这部门的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
◆ Tenured
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
◆ Young
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
6.垃圾回收过程
对于年轻代,刚开始创建的对象都是放置在eden区的,而将年轻代分成3个部分,主要是为了生命周期短的对象尽量留在年轻代。当eden区申请不到空间的时候,进行minorGC,把存活的对象拷贝到survior。年老代主要存放生命周期比较长的对象,比如缓存对象。具体jvm内存回收过程描述如下(可以结合上图):
1、对象在Eden区完成内存分配;
2、当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收;
3、minorGC时,Eden不能被回收的对象被放入到空的survivor(Eden肯定会被清空),另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的;
4、当做第3步的时候,如果发现survivor满了,将这些对象copy到old区,或者survivor并没有满,但是有些对象已经足够Old,也被放入Old区 XX:MaxTenuringThreshold;
5、当Old区被放满的之后,进行fullGC;
在知道垃圾回收机制以后,大家可以在对jvm中堆的各个参数进行优化设置,来提高性能。
7. JVM参数配置简述
JVM提供了相应的参数来对内存大小进行配置。正如上面描述,JVM中堆被分为了3个大的区间,同时JVM也提供了一些选项对Young,Tenured的大小进行控制。
◆ Total Heap
-Xms :指定了JVM初始启动以后初始化内存
-Xmx:指定JVM堆得最大内存,在JVM启动以后,会分配-Xmx参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms参数来调节真正用于JVM的内存
-Xmx -Xms之差就是三个Virtual空间的大小
◆ Young Generation
-XX:NewRatio=8意味着tenured 和 young的比值8:1,这样eden+2*survivor=1/9
堆内存
-XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor就占Young区的1/34.
-Xmn 参数设置了年轻代的大小
◆ Perm Generation
-XX:PermSize=16M -XX:MaxPermSize=64M
Thread Stack
-XX:Xss=128K
好了,以上就是关于JVM内存模型的主要分析。
1.JAVA 的内存概述:
JVM系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。当然,从进程是操作系统资源分配的单位这个角度来看,每个主内存对应于一个进程,多个线程共享该进程的资源(主内存)。
每条线程(主要处理用户定义的运算)都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。他们从主内存中取数据, 然后计算, 再存入主内存中。
当多条线程同时对主存的同一临界资源操作时,就会有线程同步问题;一些是JVM同步机制的大致过程:
(1) 获取对象监视器的锁(lock)
(2) 清空工作内存数据, 从主存复制变量到当前工作内存, 即同步数据 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 将工作内存数据刷回主存 (store and write)
(5) 释放对象监视器的锁 (unlock)
注意: 其中4,5两步是同时进行的.
这些涉及到线程同步问题,在这里就不累述了,简单了解一下。
2. 从进程和线程的角度认识堆和栈
Heap Memory(堆内存):虚拟机的堆内存保存的是对象,类变量以及实例变量,它被所有线程共享,常说的垃圾回收就是对堆内存的回收。Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。也就是上述的主内存(Java Heap Memory)。
Stack Memory 栈内存:虚拟机的每一个线程都有一个私有的栈,当一个方法被调用时,下面内容被作为一个Frame (帧)被创建并且被压入栈中:
+ 局部变量:包括基本数据类型,对象的引用和返回值地址。
+ 一个自己的操作栈:帧内局部变量进行运算时使用,也用于传递方法的参数和接受方法的返回值。
+ 一个当前方法所在类的Runtime constant pool (常量池)的引用。
方法调用完成时,帧出栈,并销毁,无论方法是正常结束还是有未捕获的异常。
Method Area 方法区(或者代码区): 当JVM加载一个class时 ,将该类的一些信息保存到Method Area,包括Runtime constant pool ,方法数据,方法和构造器代码,域等。Runtime constant pool 则 包括类名,父类名,静态变量等。Method Area在逻辑上属于Heap(因为和堆一样是被线程共享的,属于主内存)。不过它垃圾回收与Heap可能不同,取决于JVM的实现。
当通过new Class()方式创建一个实例时,JVM在Method Area寻址到该类的基本信息, 同时进行相关实例的初始化(包括实例变量),存贮在Heap中。
3. 堆和栈的进一步认识
下面我们从JVM的内存管理原理的角度来深入认识堆(Stack)和栈(Heap),并通过这些原理认清Java中静态方法和静态属性的问题。
Stack(栈)是JVM的内存指令区。Stack管理很简单,push一定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,并且每次操作的数据或者指令字节长度是已知的。所以Java 基本数据类型,Java 指令代码,常量都保存在Stack中。
Java栈是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。*Error:如果在线程执行的过程中,栈空间不够用,那么JVM就会抛出此异常,这种情况一般是死递归造成的。
Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
由于Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是随机分配内存,不定长度,存在内存分配和回收的问题;因此在JVM中另有一个GC进程,定期扫描Heap ,它根据Stack中保存的4字节对象地址扫描Heap ,定位Heap 中这些对象,进行一些优化(例如合并空闲内存块什么的),并且假设Heap 中没有扫描到的区域都是空闲的,统统refresh(实际上是把Stack中丢失了对象地址的无用对象清除了),这就是垃圾收集的过程。
4.堆栈分离的好处
从JAVA内存模型的角度去理解面向对象的设计,我们就会发现对象它完美的表示了堆和栈,对象的数据放在堆中,而我们编写的那些方法一般都是运行在栈中,因此面向对象的设计是一种非常完美的设计方式,它完美的统一了数据存储。
JVM的体系结构
我们首先要搞清楚的是什么是数据以及什么是指令。然后要搞清楚对象的方法和对象的属性分别保存在哪里。
1)方法本身是指令的操作码部分,保存在Stack中;
2)方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操作码和指令操作数构成了完整的Java 指令。
3)对象实例包括其属性值作为数据,保存在数据区Heap 中。
非静态的对象属性作为对象实例的一部分保存在Heap 中,而对象实例必须通过Stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在Stack中的地址指针。
非静态方法和静态方法的区别:
非静态方法有一个和静态方法很重大的不同:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,这个隐含的参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap 中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。
静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。当然此时静态方法是存取不到Heap 中的对象属性的。
总结一下该过程:当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。然后程序计数器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap数据区的;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到Heap 数据区了。
静态属性和动态属性:
前面提到对象实例以及动态属性都是保存在Heap 中的,而Heap 必须通过Stack中的地址指针才能够被指令(类的方法)访问到。因此可以推断出:静态属性是保存在Stack中的,而不同于动态属性保存在Heap 中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,也因此不管什么指令(类的方法),都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。
5. 再说说堆(Heap)的内部结构
前面谈到堆和垃圾回收,这里作进一步分析。
JVM堆一般又可以分为以下三部分:
Java Heap分为3个区,Young,Old和Permanent。Young(年轻代)保存刚实例化的对象。当该区被填满时,GC会将对象移到Old(年老代)。Permanent(永久代)主要是存储的是java的类信息,包括解析得到的方法、属性、字段等等。JVM的Heap分配可以使用-X参数设定,
-Xms
初始Heap大小
-Xmx
java heap最大值
-Xmn
young generation的heap大小
Older区的大小等于-Xmx减去-Xmn,不能将-Xms的值设的过大,因为第二个线程*运行会降低JVM的性能。
对照图,我们再详细了解一下。
◆ Perm
Perm代主要保存class,method,filed对象,这部门的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
◆ Tenured
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
◆ Young
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
6.垃圾回收过程
对于年轻代,刚开始创建的对象都是放置在eden区的,而将年轻代分成3个部分,主要是为了生命周期短的对象尽量留在年轻代。当eden区申请不到空间的时候,进行minorGC,把存活的对象拷贝到survior。年老代主要存放生命周期比较长的对象,比如缓存对象。具体jvm内存回收过程描述如下(可以结合上图):
1、对象在Eden区完成内存分配;
2、当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收;
3、minorGC时,Eden不能被回收的对象被放入到空的survivor(Eden肯定会被清空),另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的;
4、当做第3步的时候,如果发现survivor满了,将这些对象copy到old区,或者survivor并没有满,但是有些对象已经足够Old,也被放入Old区 XX:MaxTenuringThreshold;
5、当Old区被放满的之后,进行fullGC;
在知道垃圾回收机制以后,大家可以在对jvm中堆的各个参数进行优化设置,来提高性能。
7. JVM参数配置简述
JVM提供了相应的参数来对内存大小进行配置。正如上面描述,JVM中堆被分为了3个大的区间,同时JVM也提供了一些选项对Young,Tenured的大小进行控制。
◆ Total Heap
-Xms :指定了JVM初始启动以后初始化内存
-Xmx:指定JVM堆得最大内存,在JVM启动以后,会分配-Xmx参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms参数来调节真正用于JVM的内存
-Xmx -Xms之差就是三个Virtual空间的大小
◆ Young Generation
-XX:NewRatio=8意味着tenured 和 young的比值8:1,这样eden+2*survivor=1/9
堆内存
-XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor就占Young区的1/34.
-Xmn 参数设置了年轻代的大小
◆ Perm Generation
-XX:PermSize=16M -XX:MaxPermSize=64M
Thread Stack
-XX:Xss=128K
好了,以上就是关于JVM内存模型的主要分析。