一、jvm 运行时的内存划
JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成(年轻代和老年代的默认比例为 1:2,也就是说新生代占用 1/3的堆内存,而老年代占用 2/3 的堆内存。),而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配。
灰色区域为线程共享的数据区,浅蓝色为线程私有数据区。
程序计数器
记录当前线程所执行的字节码行号
,用于获取下一条执行的字节码
。
当多线程运行时,每个线程切换后需要知道上一次所运行的状态、位置。由此也可以看出程序计数器是每个 线程私有 的。
虚拟机栈
虚拟机栈由一个一个的栈帧
组成,栈帧是在每一个方法调用时产生的。
每一个栈帧由局部变量区
、操作数栈
等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。
每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出
StackOverflowError
。 - 若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出
StackOverflowError
。 - 若虚拟机栈允许动态扩展,但在尝试扩展时内存不足,或者在为一个新线程初始化新的虚拟机栈时申请不到足够的内存,则会抛出
OutOfMemoryError
。
这块内存区域也是 线程私有 的。
方法区(JDK1.7)
方法区主要用于存放已经被虚拟机加载的类信息
,如常量
,静态变量
。 这块区域也被称为永久代
。
可利用参数 -XX:PermSize
-XX:MaxPermSize
控制初始化方法区和最大方法区大小。
元数据区(JDK1.8)
在 JDK1.8 中已经移除了方法区(永久代),并使用了一个元数据区域进行代替(Metaspace)。
默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 java.lang.OutOfMemoryError: PermGen。
但也不能无限扩展,因此可以使用 -XX:MaxMetaspaceSize
来控制最大内存。
Java 堆
Java 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。
可利用参数-Xms
-Xmx
进行堆内存控制。
这块区域也是垃圾回收器
重点管理的区域,由于大多数垃圾回收器都采用分代回收算法
,所有堆内存也分为新生代、老年代
,可以方便垃圾的准确回收。
运行时常量池
运行时常量池是方法区的一部分,其中存放了一些符号引用。当 new 一个对象时,会检查这个区域是否有这个符号的引用。
直接内存
直接内存又称为 Direct Memory(堆外内存
),它并不是由 JVM 虚拟机所管理的一块内存区域。
有使用过 Netty 的朋友应该对这块并内存不陌生,在 Netty 中所有的 IO(nio) 操作都会通过 Native 函数直接分配堆外内存
。
它是通过在堆内存中的 DirectByteBuffer 对象操作的堆外内存,避免了堆内存和堆外内存来回复制交换复制,这样的高效操作也称为零拷贝。
既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 JVM 管理,所以常规 GC 操作并不能回收堆外内存。它是借助于老年代产生的 fullGC 顺便进行回收。同时也可以显式调用 System.gc() 方法进行回收(前提是没有使用-XX:+DisableExplicitGC
参数来禁止该方法)。
值得注意的是:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。
常用参数
-
-Xms
: 设置堆的最小空间大小。 -
-Xmx
: 设置堆的最大空间大小。 -
-XX:NewSize
: 设置新生代最小空间大小。 -
-XX:MaxNewSize
: 设置新生代最大空间大小。 -
-XX:PermSize
: 设置永久代最小空间大小。 -
-XX:MaxPermSize
: 设置永久代最大空间大小。 -
-Xss
:设置每个线程的堆栈大小。
-Xms64m //最小堆内存 64m.
-Xmx128m //最大堆内存 128m.
-XX:NewSize=30m //新生代初始化大小为30m.
-XX:MaxNewSize=40m //新生代最大大小为40m.
-Xss=256k //线程栈大小。
-XX:+PrintHeapAtGC //当发生 GC 时打印内存布局。
-XX:+HeapDumpOnOutOfMemoryError //发送内存溢出时 dump 内存。
新生代和老年代的默认比例为 1:2,也就是说新生代占用 1/3的堆内存,而老年代占用 2/3 的堆内存。
可以通过参数 -XX:NewRatio=2
来设置老年代/新生代的比例。
二、垃圾回收
垃圾回收主要思考三件事情:
- 哪种内存需要回收?
- 什么时候回收?
- 怎么回收?
判断对象是否存活
一般有两种方式:
引用计数 : 每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
如图:Object1、2、3、4 都是存活的对象,而 Object5、6、7都是可回收对象。
在Java语言中,GC Roots包括 :
- 虚拟机栈中引用的对象。
- 方法区中类静态属性实体引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
垃圾回收算法
- 标记 -清除算法
- 复制算法
- 标记整理算法
- 分代回收算法
分代回收算法:
现代多数的商用 JVM 的垃圾收集器都是采用的分代回收算法,和其他的算法并没有新的内容。
只是将 Java 堆分为了新生代和老年代。由于新生代
中存活对象较少,所以采用复制算法,简单高效。
而老年代
中对象较多,并且没有可以担保的内存区域,所以一般采用标记清除或者是标记整理算法。
垃圾收集器
- Serial收集器
- Parallel收集器
- Parallel Old 收集器
- CMS收集器
- G1收集器