1.物理内存以及虚拟内存
- 在java中,分配内存和回收内存都由JVM自动完成,甚至不需要写和内存相关的代码
- 物理内存即RAM还有寄存器(一种存储单元,用于存储计算机单元执行指令(如整形浮点等运算)的中间结果)是处理器通过地址总线连接的。地址总线:其宽度决定了一次可以存寄存器或者RAM中获取多少个bit和处理器最大的可以寻址的范围,每个地址会引用一个字节,所以如果是32位的总线则可以有4G的内存空间。(通常情况下地址总线和RAM或寄存器有相同的位数)
- 通常操作系统的内存申请空间是按照进程来管理的,每个进程间不会互相重合,操作系统保证每个进程拥有一段独立的地址空间。(逻辑上独立,物理空间不一定独立,如虚拟内存,虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。)
- 由于程序越来越庞大何设计的多任务性,物理内存无法满足要求,出现了虚拟内存,虚拟内存使得多个进程可以共享物理内存,并且逻辑上独立。虚拟内存提高了内存利用率,并且可以扩展内存空间,使得一个虚拟的地址可以映射到物理内存,文件或者其他可以寻址的存储上。如一个进程在不活动的情况下,操作系统将这个物理内存中的数据移到一个磁盘文件下(频繁地交换物理内存和磁盘上的数据,会导致效率低下,需要关注)
2. 内核空间和用户空间
电脑的内存地址空间将被划分为内核地址空间和用户空间,程序只能使用用户空间的内存(指程序能够申请的内存)(如windows32为默认内核空间和用户空间的比例是1:1,linux32为默认的比例是1:3)
内核空间主要指操作系统用于程序调度、虚拟内存或者连接硬件资源等的程序逻辑。程序不能访问操作系统的空间,并且不能直接访问硬件资源,必须通过系统提供的接口调用。(每一次系统调用都会引起内核空间和内存空间的切换,这一操作比较耗时).
3. Java需要内存的组件
-
堆
用于存储java对象的内存区域,可以通过Xmx(最大大小)和Xms(初始大小)来控制大小,默认空余堆内存少于40%时就扩大到Xmx,空余堆内存大于70%时就缩小到Xms,因此,服务器一般把xmx和xms设置成一样,避免在GC后调节堆的大小。
-
栈
每个线程创建时,JVM都会为它创建一个运行方法栈、局部变量的堆还有操作栈。
-
类和类加载器
类和类的加载器本身同样需要存储空间,存储在永久代PermGen(属于方法区,即java堆的永久区部分)
ps:
JVM是按需加载类的,隐式加载只会加载那些应用程序中明确使用到的。
加载类超过PermGen区大小的话可能会导致内存溢出,所以对于自己实现的类加载器可能会导致类的重复加载时,可能需要实现对类的卸载,需满足:
Java堆中没有对表示该类的类加载器的java.lang.ClassLoader对象的引用,
Java对中没有该类的对应加载器的java.lang.class对象的引用,
Java堆上任何该类的类加载器的任何类的所有对象都不存活。
而JVM的默认类加载器都不满足该条件,所以他们加载的类都不能卸载。 -
NIO
NIO使用ByteBuffer.allocateDirect()方法分配内存,可以避免数据从内核空间到用户空间的复制,提高效率,但是该方法直接使用的是本机内存而不是java堆内存,直接的ByteBuffer对象可以自动清理本机缓存区,但是其只是作为GC时的一部分执行,而GC只在Java堆被填满或者显示调用System.gc()来执行(也就是自动的GC只检查Java堆是否满,而不知道NIO操作的本机内存是否需要释放。),以至于NIO在很多框架中是通过显示调用System.gc()执行NIO内存的释放的.
-
JNI
JNI使得本机代码(如C语言)可以调用java方法,JVM会准备空间以供运行本地方法,也会增加java运行时的本机内存占用.
4. Jvm内存结构
JVM是按照运行时数据的存储结构来划分内存结构的,根据不同的格式存储在不同的区域。运行时数据包括java程序本身的数据信息和JVM运行Java程序需要的额外数据信息,java虚拟机规范将Java运行时数据分为6种:PC寄存器、Java栈、堆、方法区、本地方法区、运行时常量池。
-
PC寄存器
用于保存当前执行程序的内存地址,也就是记录某线程当前执行的方法的那一条指令,如线程的执行被中断后就会依靠这些数据来恢复(JVM规范之定义了对java方法需要记录指针,对本地方法则没有规范).
-
Java栈
java栈与线程相关联,每创建一个线程就会为该线程创建一个栈,而线程中运行的每一个方法则与栈中的每一个栈帧关联起来,栈帧中包含局部变量,操作栈,方法返回值等。每一个方法完成,就会弹出栈帧的元素(操作栈的栈顶元素),作为返回值,清除这个栈帧。java栈的栈顶就是当前正在执行的活动栈,PC寄存器会指向这个方法的地址。Java栈和线程对应起来,这些数据不是线程共享的,不存在一致性问题.
-
堆
存储Java对象的地方,由于是所有线程共享的,所以需要关心数据的一致性问题。
-
方法区
用于存储类结构信息,如常量池、域,方法数据、方法体,构造函数、包括类中的专用方法、实力初始化、接口初始化等
- 方法区同样属于java堆的永久代
- 如果使用动态编译时要注意这部分是否能满足类的存储
- 这个区域并不像其他java堆一样频繁地被GC回收
-
运行时常量池
包括编译器的数字常量,方法或者域的引用。(注意,这一区域属于方法区)
-
本地方法栈
JVM为运行native方法准备的空间。由于很多native方法是用c语言实现的,所以又叫C栈。这个区域jvm并没有严格的限制,由不同的JVM实现者自由实现。
5.Jvm内存分配策略
- 静态内存分配策略:在编译期间必须知道内存空间(8个基本类型)的大小才可以分配(所以可以在编译期间分配内存,但java栈中的局部变量和引用等数据同样使用静态内存分配,该空间大小是在编译期间知道,但是在程序加载时才正式分配的,并且这一部分内存在java栈上分配),不允许可变数据类型或者递归、嵌套等结构的出现。
- 栈内存分配:不需要在编译时知道程序对数据的需求、但在进入程序模块时必须知道数据的要求才可以分配内存。并且按照后进先出的原则进行内存的分配
- 堆内存分配:可以在运行到相应代码才知道内存空间的大小,灵活但是效率较差
JVM的内存分配主要基于堆和栈 :
栈:
- 栈的分配时和线程绑定的,为每一个线程创建一个栈,为线程每调用一个新的方法创建一个栈帧
- 栈中主要保存基本类型数据和对象的句柄(引用、指针),栈的数据大小和生存期都必须是确定的,而本地变量和操作栈的大小都可以在编译时(class字节码)确定
- 存取速度比堆要快,仅次于寄存器,这也是为什么运算要留在操作栈中执行
- 栈的内存分配是在程序运行时进行的,只是分配的大小是在编译时确定的
堆:
- 堆可供所有线程访问,主要存放实例数据,由于时动态分配内存大小的,所以存取速度较慢,同样通过GC回收内存
- 新对象如何分配内存:根据对应Constant_Class_info类型数据执行new指令,赋值,调用init初始化构造器最后才赋值给变量(所以在初始化完成前不应该把实例指针公布,可类比“对象逸出”的问题),栈中存放的只是指针(引用),而真正的实例数据是存放在堆中的
- 堆在运行时请求操作系统分配内存,灵活但效率低
6. Jvm内存回收策略
静态内存的分配和回收:类中的局部变量和对象的引用都是静态内存分配的(这一部分内存空间在栈上分配),在编译时这一部分空间已经确定,只是在程序被加载时一次性分配,而当方法运行结束时随着对应栈帧的撤销回收。
动态内存分配和回收:像实例等数据只有在JVM解析类对象后才能知道具体需要分配多少空间,并且堆中的这些数据只有在对象不再被引用时才会被回收。只要某个对象不再被其他活动对象所引用就可以被回收,而活动对象是指可以被根集合对象所到达的对象。根集合对象所包含的对象跟jvm具体实现有关,但是大都会包含如下一些元素:方法中局部变量的引用、java操作栈中的对象引用、常量池中的对象引用、本地方法中持有的对象引用、类的class对象(当该Class对象不再被使用时同样会被回收)。
基于分代的垃圾收集算法:分为young、old、perm三个区
young区分为eden区和两个survivor区,eden区满后会触发minorGC,minorGC后仍存活的对象将放到survivor区(若另一个survivor区存在活动对象将放到同一个区中,保证一个survivor区是空的)
old区中已满将会触发FullGC,old区中存放的是:
- Young的survivor区中已满后minorGC仍然存活的对象
- survivor区中足够老的对象Eden中已满,并且minorGC后存在,并因为servivor已满无法存放的对象。
- perm区主要存放类的class对象,只有在FullGC时才会被回收