1、JVM是如何管理内存的
Java中,内存管理是JVM自动进行的,无需人为干涉。
了解java内存模型看这里:java内存模型是什么样的
了解jvm实例结构看这里:jvm实例的结构是什么样的
创建对象或者变量时, JVM会自动分配内存(当然这个分配是遵循严格规则的)。当JVM发现某些对象不再需要的时候,就会对该对象占用的内存进行重分配(释放)操作,而且使得分配出来的内存能够提供给所需要的对象。
在这个方面,其他一些编程语言里面,内存管理是程序员的职责,程序员是需要手动管理内存。这一点C++的程序员很清楚,最终大部分开发时间都花在了调试这种内存管理程序以及修复相关错误上。
了解显示内存管理的弊端看这里:显示内存管理有什么弊端
2、JVM内存组成结构
了解内存分配策略看这里:内存分配有哪些策略
JVM的内存组织需要在不同的运行时数据区进行操作。包括PC寄存器(计数器 pc registers)、方法区(method area)、本地方法栈(native method stacks)、栈(stacks)、堆(heap)。如图:
PC寄存器(计数器 pc registers)
每个新的线程启动后,它就会被JVM在内部分配自己的PC寄存器(计数器 pc registers),通过计数器来指示下一条指令执行。
方法区(method area)
方法区是用来储存类的装载信息的。当JVM加载一个类的时候,它定位到对应路径里去查找对应的Class文件,类加载器读取类文件(线性二进制数据),然后将该文件传递给JVM,JVM从二进制数据中提取信息并且将这些信息存储在方法区。
在JVM内部,所有的线程共享相同的方法区。需要注意的是:类中的静态变量(类变量)就是储存在方法区中的(了解静态变量看这里:局部变量、类变量、实例变量有什么区别)。
方法区中除了有类文件信息外,还包含一个运行时常量池(Runtime Constant Pool),用来存放基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(通过String.intern()方法可以强制将String放入常量池)。之所以称之为动态,是因为不单能在编译期产生常量,运行期间也可以,当然运行时常量池同样是所有线程共享。(Java虚拟机对Class文件的每一部分的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范,这样才会被虚拟机装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。)
由于运行时常量池是方法区的一部分,所以会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError: PermGen space异常(Java 8以后没有方法区,由本地元空间代替,溢出会抛出OutOfMemoryError: Metaspace异常)。
本地方法栈(native method stacks)
若某线程正在执行一个本地Java方法,该线程的本地方法内存栈中,保存了本地Java方法调用状态,其状态包括局部变量、被调用的参数、它的返回值、以及中间计算结果。
在这种情况下,使得这些本地方法和其他内存数据区的内容尽可能独立,而且这些本地方法执行的字节码,有可能根据操作系统环境的不同,使得其编译出来的本地字节码的结构也有一定的差异。
栈(stacks)
-
对于线程内存栈分配:当一个新线程启动的时候,JVM会为Java线程创建每个线程的独立内存栈。内存栈是由栈帧构成,在JVM里面,栈帧的操作只有两种:出栈和入栈。正在被线程执行的方法称为当前线程方法,而该方法的栈帧就称为当前帧,而在该方法内定义的类称为当前类。
-
对于方法:当一个线程调用某个Java方法时,Jvm创建并将一个新帧压入到内存栈中,这个帧成为当前栈帧,当该方法执行的时候,JVM使用内存栈来存储参数引用、局部引用变量、基本类型数值、中间计算结果以及其他相关数据。
方法在执行过程有可能因为两种方式而结束:如果一个方法返回,属于正常结束;如果在这个过程抛出异常而结束,为异常结束。不论是正常结束还是异常结束,JVM都会弹出或者丢弃该栈帧,则上一帧的方法就成为了当前帧。 -
对于线程:在JVM中,Java线程的栈数据是某个线程独有的,其他的线程不能修改或访问该线程的栈帧。当一个线程调用某个方法的时候,方法的局部变量是在线程独有的栈帧存储,只有当前线程可以访问该局部变量, 所以不用担心多线程同步访问Java的局部变量。
-
对于容量:编程过程,允许指定Java栈的初始大小以及最大、最小容量。
堆(heap)
-
对于对象内存堆分配:当一个Java程序创建一个对象或者一个数组时,JVM实例会针对该对象和数组分配一个新的内存堆空间。在JVM实例内部,只存在一个内存堆的实例,所有的依赖该JVM的Java程序都共享该实例。
-
对于进程内存堆分配:在Java程序启动的时候,会得到JVM分配的属于自己的堆空间,而且针对每一个Java应用程序,这些运行Java程序的堆空间都是相互独立的。
-
对于内存堆共享:上述两种分配并不冲突,JVM在初始化运行的时候整体堆空间只有一个,这个是Java语言平台直接从操作系统上能够拿到的整体堆空间(不会超过物理内存最大值),所以的依赖该JVM的程序都可以得到这些内存空间。但是针对每一个独立的Java程序而言,这些堆空间是相互独立的,每一个Java应用程序在运行最初都是依靠JVM来进行堆空间的分配的。
-
对于进程:两个相同的Java程序,在运行时处于不同的进程中(一般为java.exe),它们各自分配的堆空间都是独立的,不能相互访问。只是两个Java进程初始化拿到的堆空间都是来自JVM的分配(从最初的内存堆实例里面分配出来的)。
-
对于线程:在同一个Java进程中,不同的线程是可以共享每一个Java程序拿到的内存堆空间的。这也是为什么在开发多线程程序的时候,针对同一个Java程序必须考虑线程安全问题,因为在一个Java进程里,所有的线程是可以共享这个Java进程堆空间中的数据的。了解进程和线程看这里:线程和进程有什么区别
- 堆内存释放:JVM拥有针对新的对象分配内存的指令,但是却不包含释放该内存空间的指令。当然开发过程可以在Java源代码中显示释放内存或者说在JVM字节码中进行显示的内存释放,但是JVM仅仅只是检测堆空间中是否有引用不可达(不可以引用)的对象,然后将接下来的操作交给垃圾回收器(GC)来处理。