1 物理内存与虚拟内存

  1. 物理内存就是RAM(随机存储器),还有一个存储单元叫做寄存器,连接处理器和RAM或者寄存器的是地址总线,这个地址总线的宽度影响了物理地址的索引范围,同时也决定了处理器最大可以寻址的地址空间。
  2. 除了硬件程序或者驱动程序需要直接访问存储器外,大部分情况下都是通过操作系统提供的接口来访问内存,在java中甚至不需要写和内存相关的代码。
  3. 我们要运行程序,都要向操作系统先申请内存地址,每个进程拥有一段独立的地址空间,操作系统也会保证每个进程只能访问自己的内存空间。
  4. 虚拟内存的出现使得多个进程在同时运行时可以共享物理内存,虚拟地址不但可以让进程共享物理内存,提高内存利用率,而且还能扩展内存的地址空间。如:一个进程在不活动的情况下,操作系统将这个物理内存中的数据移到一个磁盘文件中,而真正高效的物理内存留给正在活动的程序使用。

2 内核空间与用户空间

内核空间主要是操作系统运行时所使用的用于进程调度,虚拟内存的使用或者硬件资源等的程序逻辑。
为了保证系统的稳定性和安全性,所以分为两个空间,如访问硬件资源只能由操作系统来完成,用户程序不允许直接访问硬件资源。
如果用户需要访问硬件资源,如网络连接等,可以调用操作系统提供的接口来访问。
在执行系统调用的时候,需要在两个内存空间进行切换、复制,虽然保证了程序运行的稳定性和安全性,但是也牺牲了一部分效率,Linux系统提供了sendfile文件传输方式来减少复制带来的开销。

3 在Java中哪些组件需要使用内存

Java堆

Java堆是用于存储Java对象的内存区域,堆的大小在JVM启动时就一次向操作系统申请完成,通过-Xmx和-Xms两个选项来控制。-xmx表示堆的最大值,xms表示初始大小,一旦分配完成,堆空间就固定了。不能重新申请。

线程

JVM运行实际程序的实体是线程,每个线程创建时JVM都会为它创建一个堆栈。
如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致比较差的性能和更高的内存占用率。

类和类加载器

JVM是按需加载类的,JVM只会加载那些在你的应用程序中明确要使用的类到内存中,要查看JVM到底加载了哪些类,可以在启动参数上加上-verbose:class
如果使用自定义的类加载器来加载类,可能会出现重复加载的情况,就有可能导致PermGen区内存泄漏。
通常一个类能够被卸载,需要满足如下条件:

1:在Java堆中没加载有该类的classloader对象的引用。

2:java堆上没有加载该类的classloader已加载的类的class对象引用

3:java堆上没有加载该类的类加载器加载的任何类的对象

由于jvm创建的三个默认类加载器都不可能满足这些条件,所以任何系统类加载器加载的类都不能在运行时被释放。

NIO

NIO引入了一种基于通道和缓存区来执行I/O的新方式;

NIO使用java.nio.ByteBuffer.allocateDirect()方法分配内存;

ByteBuffer.allocateDirect()分配的内存使用的是本机内存而不是Java堆上的内存,每次分配内存时会调用操作系统的os:malloc()函数;

ByteBuffer产生的数据如果和网络或者磁盘交互都在操作系统的内核空间中发生,不需要将数据复制到Java内存中,避免了在Java堆与本机堆之间复制数据;

很多NIO框架都在代码中显式地调用System.gc()来释放NIO持有的内存,但是这样做回影响应用程序的性能,还有可能导致内存泄漏。

JNI

JNI技术使得java代码可以调用本机代码(比如c语言程序)。这部分用到了native memory,也就是本地内存。

4 JVM内存结构

PC寄存器(程序计数器)

它用于保存当前正常执行的程序的内存地址,当有多个线程交叉执行时,被中断线程的程序当前执行到哪条的内存地址必然要保存下来,以便于它被恢复执行时再按照被中断时的指令地址继续执行下去。

Java虚拟机栈

1 Java的栈和线程关联在一起。

2 每个线程有一个栈。

3 每运行一个方法就创建一个新栈帧。

4 栈帧包含了内部变量(方法内部的变量,不是方法参数,方法参数用调用者传来),操作数栈,方法返回值等信息。

5 每个方法执行完成时,每个栈帧都会弹出栈帧的元素作为方法的返回值。

6 java栈的栈顶就是当前的活动栈,pc寄存器会指向这个地址。

7 线程私有,不用担心数据一致性问题,也不会存在同步锁的问题

堆是存储对象的地方,每一个存储在堆中的对象都是这个对象类的一个副本,它会复制包括继承它的父类的所有非静态属性。
注意是非静态属性,静态属性编译时确定,存在类的元数据中,在方法区。

方法区

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,是线程共享的内存区域。
方法区存储的数据比较稳定,不会被频繁回收。

运行时常量池

运行时常量池时方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

本地方法栈

本地方法栈是为JVM运行本地方法(Native)方法准备的空间,跟Java栈的作用类似。

5 JVM内存分配策略

通常的内存分配策略

在操作系统中内存分配策略分为三种,分别是:

  • 静态内存分配
  • 栈内存分配
  • 堆内存分配
    静态内存分配是指在程序编译时就能确定每个数据在运行时的存储空间需求,因此在编译时就可以给它们分配固定的内存空间。
    栈内存分配也可以称为动态存储分配,是由一个类似于堆栈的运行栈来实现的,按照先进后出的原则进行分配。
    堆内存分配是在程序运行时才执行的,它的运行效率也是比较差的。

    Java中的内存分配详情

    Java栈的分配是和线程绑定在一起的,没创建一个线程,虚拟机就为其分配一个Java栈,一个线程的方法的调用和返回对应这个Java栈的压栈和出栈。当线程激活一个Java方法时,JVM就会在线程的Java堆栈里新压入一个帧,这个帧用来保存参数,局部变量,中间计算过程和其他数据。
    栈中主要存放一些基本类型的数据变量(int、short、long、byte、float、double、boolean、char)和对象句柄(引用)。
    优点:存取速度快,数据可以共享;
    缺点:存在栈中 的数据大小与生存期必须确定,缺乏灵活性。

每个Java应用都唯一对应一个JVM实例,每个实例唯一对应一个堆,应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用程序所有的线程共享。
所有对象的存储空间都是在堆中分配,但是这个对象的引用却是在堆栈中分配;
堆的优势是可以动态地分配内存大小;
缺点是存取速度慢。

从栈和堆的功能和作用来通俗的比较:堆主要用来存放对象,栈主要用来执行程序。

6 JVM内存回收策略

通常显式的内存申请有两种:静态内存分配和动态内存分配

静态内存分配和回收

在Java中静态内存分配是指在Java被编译时就已经能够确定需要的内存空间,当程序被加载时系统把内存一次性分配给它。、
在Java的类和方法中的局部变量包括原生数据类型和对象的引用都是静态分配内存。
静态内存空间是在Java栈上分配的,当着方法运行结束时,对应的栈帧也就撤销,所以分配的静态内存空间也就回收了。

动态内存分配和回收

基本数据类型存储在Java栈中,方法执行结束就会消失,而对象类型存储在Java堆中,是可以被共享的,也不一定随着方法执行结束而被消失。
动态分配是程序只有在执行时才知道要分配的存储空间大小,而不是在编译时就能够确定的。
内存的回收是以对象不再引用为前提的。

如何检测垃圾

只要对象不再被其他活动对象引用,那么就可以被回收,这里的活动对象是指能够被一个根对象集合到达的对象,也就是我们通常所说的可达性分析。
除了可达性分析外,还有一种方法叫做引用计数法,但是不推荐使用,因为相互引用会导致无法被垃圾收集器回收,从而可能造成内存泄漏。

基于分代的垃圾收集是算法

把对象按照寿命长短来分组,分为年轻代和年老代,如果对象经过几次回收后仍然存活,那么再把这个对象划分到年老代。
JVM将整个堆划分为Young区、Old区和Perm区,分别存放不同年龄的对象。
Young区分为
Young区分为Eden区和两个Survivor区,其中所有新建的对象都放在Eden区,当Eden区满后会触发minor GC将Eden区仍然存活的对象复制到其中一个Survivor区中,另外一个Survivor区中的存活对象也复制到这个Survivor中,以保证始终有一个Survivor区是空的。
Old区存放的是Young区的Survivor满后触发minor GC后仍然存活的对象,当Eden区满后会将对象存放到Survivor区中,如果Survivor区仍然存放不下这些对象,GC收集器会将这些对象直接放到Old区,如果在Survivor区中的对象足够老,也直接放到Old区,如果Old区也满了,将会触发Full GC,回收整个堆内存。
Perm区存放的主要是类的Class对象,如果一个类被频繁的加载,也可能导致Perm区满,Perm区的垃圾回收也是由Full GC触发的。
建议:Young区的大小为整个堆的1/4,而Young区的Survivor区一般设置为整个Young区的1/8。