原创JudyGril
JVM内存区域
程序运行会将编译好的.class 文件(静态),想要运行必须装载到JVM内存中,通过ClassLoad加载到JVM内存区域,将.class文件加载到内存中形成逻辑映射,简单来说程序运行需要:1.数据,2.执行方法,3.从哪个指令开始执行
思维导图
内存区域图
内存区域发展历史及原因
JVM内存区域中方法区实现发生过变化, 在JDK1.8之前使用的是HotSpot虚拟机 开发 , 使用永久代来实现方法区, 实际上实现方法区不用做统一的规范,对于其它虚拟机根本就没有永久代的概念, 所以有的时候如果源头理不清楚我们会混乱知识结构, 方法区不等于永久代
JDK6的时候逐步放弃永久代改为本地内存
JDK7把原本放在永久代常量池,静态变量移除,存放在堆中
JDK8彻底放弃永久代使用元空间(本地内存)
为什么要永久代替换成元空间 ?
如果常量池存储在永久代容易出现溢出
内存区域包含哪几部分
程序计数器
占用一小块内存空间, 是当前线程所执行字节码的行号指示器,字节码运行过程中通过改变程序计数器的值来获取下一条字节码指令, 该字节码指令对应的是一个线程, 另一个线程是无法获取的, 否则就乱套了, 所以程序计数器是线程私有内存.
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: iconst_2
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: aload_1
11: invokevirtual #3 // Method java/lang/Integer.intValue:()I
14: aload_2
15: invokevirtual #3 // Method java/lang/Integer.intValue:()I
18: iadd
19: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: astore_3
23: return
}
虚拟机栈
虚拟机栈针对的是Java方法执行的内存模型,所以他也是线程私有的. 每个方法在执行的时候都会创建栈帧, 栈帧里存储的是方法信息. 每个方法调用都对应着入栈和出栈
- 局部变量表
存储的是方法变量,方法参数
- 操作数栈
底层数据结构也是栈, 主要用途为 操作数据运行, 当方法运行的时候存储一个栈帧, 栈帧里 操作数栈内存为空, 当有变量进行运算的时候, 变量入栈到操作数栈, 等运算完结果进行出栈操作,把数据返回给变量或方法调用者.
- 动态链接
在类加载阶段或者第一次使用的时候方法区中的符号引用(符号引用在常量池中)转换为直接引用
- 返回地址
方法执行完成之后,返回方法调用的地方(程序计数器+栈帧)
- 异常情况
由于虚拟机栈是线程私有的,每一个方法允许都会创建 栈帧 , 如果递归运行无限创建栈帧, 最终会超出虚拟机所允许的深度, 会报出stackOverflowError
本地方法栈
为什么有虚拟机栈还会有本地方法栈呢? 他们的区别在于 虚拟机栈是为虚拟机执行的方法, 而本地方法栈是Native方法
堆
堆是线程共享的一个内存区域, 在虚拟机启动的时候创建, 它主要是为存储对象实例, Java堆还有另一个知识点是垃圾收集器, 目前 先不在这里做简述.
堆中在jdk7的时候把常量池移动到堆中.
对象的内存布局
当对象被创建的时候会被分配到堆中, 对象有三部分组成, 对象头, 实例数据, 对象填充, 对象头是由两部分组成,1存储对象运行时数据, 例如GC年龄 , 是否持有锁, 锁状态 2 类型元数据指针,以便于区分该对象是属于什么类型的对象.那个类的实例
方法区
方法区与堆一样都是内存共享区域, 有一个别名叫做非堆,主要目的是为了区分堆. 字符串常量和静态变量在jdk7的时候已经移动到堆中,方法区存储实现是元空间,元空间使用的是本地内存存储.
直接内存(堆外内存)
直接内存并不是JVM内存区域的一部分,但它也会频繁的调用, 在JDK1.4的时候引入了NIO, NIO通过Channel与Buffer可以使用Native函数库来分配对外内存, 通过DirectByteBuffer对象作为这块内存的引用来进行操作,这样避免了Java堆和native堆中来回复制数据。
为什么使用堆外内存
1 减少垃圾回收
2 提升复制速度
堆内内存有JVM管理,属于用户态,堆外内存由操作系统管理属于内核态,如果从堆内向磁盘写数据时,数据先被复制到堆外内存(内核缓冲区),然后再由操作系统写入磁盘,所以如果直接使用堆外内存可以避免这个过程,所以提高复制速度。
while (true){
//创建DirectByteBuffer对象
ByteBuffer.allocateDirect(10 * 1024 * 1024);
}
// 创建DirectByteBuffer对象
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
//内存是否按也分配对齐
boolean pa = VM.isDirectMemoryPageAligned();
//获取每页大小
int ps = Bits.pageSize();
//如果是按页对齐的,多分配一页容量(因为后续可能需要按页对齐)
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
//分配本地内存也就是直接内存,base表示直接内存起始地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 为申请的内存批量填充0值
unsafe.setMemory(base, size, (byte) 0);
//是否要求地址按页向上对齐
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 用清理器追踪对象,当引用失效时,回收内存
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Java对象创建过程
public class JVMShowcase {
//静态类常量,
public final static String ClASS_CONST = "I'm a Const";
//私有实例变量
private int instanceVar=15;
public static void main(String[] args) {
//调用静态方法
runStaticMethod();
//调用非静态方法
JVMShowcase showcase=new JVMShowcase();
showcase.runNonStaticMethod(100);
}
//常规静态方法
public static String runStaticMethod(){
return ClASS_CONST;
}
//非静态方法
public int runNonStaticMethod(int parameter){
int methodVar=this.instanceVar * parameter;
return methodVar;
}
}
JVM运行时内存
上篇文章介绍JVM区域都有什么, 存储什么数据. 这篇文章讲的是JVM堆中对象的内存分配 . 在堆中Java对象可以分为两大类,新生代和老年代
宏观图
新生代
新生代表示新生成的对象会存放到新生代.一般对象在新生代的时候就会被回收GC
Eden区
对象创建的时候先存储在新生代Eden区, Eden区是有空间限制的, 如果空间满了会触发GC, 没有引用的对象这个时候会被回收, 如果有存留下来的幸存者 ,包含ServivorFrom,ServivorTo,占比8:1:1
老年代
在新生代GC多次之后(15)则会进入老年代,在老年代存活的时间长,因为空间比较大,GC的几率不是很高
举例子
新生代GC条件
解说一下内容
堆最大为40M,新生代大小为20M,PrintGCDetails为打印日志。
alloc1,alloc2 , alloc3 总共加起来 15 , 满足新生代20M ,当alloc4加载的时候发现无法放入,这个时候新生代触发GC,[DefNew: 12216K->592K(18432K), 0.0073585 secs] , 12216GC之后变为592
def new generation total 18432K, used 16594K 总共需要18432K,eden占用16384K,from占用2048K,to占用2048K
由于Servivor区空间也不够使用,所以对象进入老年代, the space 20480K, 50% used
/**
- @Author: judy
- @Description:参数设置:-verbose:gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8
- @Date: Created in 8:09 2020/7/9
*/ public class demo { public static void main(String[] args) { final int tenMB = 1024 * 1024; byte[] alloc1, alloc2, alloc3, alloc4; alloc1 = new byte[5 * M]; alloc2 = new byte[5 * M]; alloc3 = new byte[5 * M]; alloc4 = new byte[10 * M]; } } 运行结果: [GC (Allocation Failure) [DefNew: 12216K->592K(18432K), 0.0073585 secs] 12216K->10832K(38912K), 0.0074091 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] Heap def new generation total 18432K, used 16594K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000) eden space 16384K, 97% used [0x00000000fd800000, 0x00000000fe7a09f0, 0x00000000fe800000) from space 2048K, 28% used [0x00000000fea00000, 0x00000000fea94178, 0x00000000fec00000) to space 2048K, 0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000) tenured generation total 20480K, used 10240K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000) the space 20480K, 50% used [0x00000000fec00000, 0x00000000ff600020, 0x00000000ff600200, 0x0000000100000000) Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K class space used 350K, capacity 388K, committed 512K, reserved
对象进入老年代
大对象直接进入老年代
长期存活对象进入老年代
总结就是空间不够用的时候进入老年代
**1 .**当对象大小等于或大于Eden区,这个时候会抛出异常,因为空间不够使用, 你会不会思考为什么不直接进入老年代呢?因为系统不认为他是大对象。
public class Demo {
private static final int M = 1024 * 1024;
public static void main(String[] args) {
byte[] alloc1, alloc2, alloc3, alloc4;
alloc4 = new byte[20 * M];
}
}
[GC (Allocation Failure) [DefNew: 1648K->581K(18432K), 0.0022828 secs][Tenured: 0K->580K(20480K), 0.0026501 secs] 1648K->580K(38912K), [Metaspace: 3068K->3068K(1056768K)], 0.0050093 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 580K->562K(20480K), 0.0027778 secs] 580K->562K(38912K), [Metaspace: 3068K->3068K(1056768K)], 0.0028232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Demo.main(Demo.java:14)
Heap
def new generation total 18432K, used 819K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
eden space 16384K, 5% used [0x00000000fd800000, 0x00000000fd8cced0, 0x00000000fe800000)
from space 2048K, 0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
to space 2048K, 0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
tenured generation total 20480K, used 562K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
the space 20480K, 2% used [0x00000000fec00000, 0x00000000fec8cb90, 0x00000000fec8cc00, 0x0000000100000000)
Metaspace used 3155K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 345K, capacity 388K, committed 512K, reserved 10
**2 .**当设置大对象大小的时候,判断大对象是否可以直接进入老年代,不会抛出溢出异常
-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1024000
可以看出from space 和 to space都是0并且也没有触发GC, 老年代 tenured generation total 20480K, used 5120K,使用了5120,说明对象直接进入老年代。
public class Demo {
private static final int M = 1024 * 1024;
public static void main(String[] args) {
byte[] alloc1, alloc2, alloc3, alloc4;
alloc4 = new byte[5 * M];
}
}
Heap
def new generation total 9216K, used 1858K [0x00000000fe200000, 0x00000000fec00000, 0x00000000fec00000)
eden space 8192K, 22% used [0x00000000fe200000, 0x00000000fe3d0ab0, 0x00000000fea00000)
from space 1024K, 0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000feb00000)
to space 1024K, 0% used [0x00000000feb00000, 0x00000000feb00000, 0x00000000fec00000)
tenured generation total 20480K, used 5120K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
the space 20480K, 25% used [0x00000000fec00000, 0x00000000ff100010, 0x00000000ff100200, 0x0000000100000000)
Metaspace used 3139K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
3 . 当对象超过一定年龄则会进入老年代(默认年龄为15,每发生一次GC则会触发一次GC)
-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
首先我们设置对象的年龄为1则进入老年代, eden space 838912K, 100% 可以看出eden区内存已经全部占用,但是from和to都是0% from space 104832K, 0% to space 104832K, 0% ,老年代使用的空间为tenured generation total 1048576K, used 614400K ,所以可以看出对象在第一个gc的时候直接进入老年代。满足对象GC年龄为1则进入老年代。
public class Demo {
private static final int M = 1024 * 1024;
public static void main(String[] args) {
byte[] alloc1, alloc2, alloc3, alloc4;
alloc1 = new byte[300 * M];
alloc2 = new byte[300 * M];
alloc3 = new byte[300 * M];
alloc4 = new byte[500 * M];
}
}
[GC (Allocation Failure) [DefNew: 664734K->616K(943744K), 0.3629964 secs] 664734K->615016K(1992320K), 0.3630391 secs] [Times: user=0.13 sys=0.23, real=0.36 secs]
Heap
def new generation total 943744K, used 839528K [0x0000000080000000, 0x00000000c0000000, 0x00000000c0000000)
eden space 838912K, 100% used [0x0000000080000000, 0x00000000b3340000, 0x00000000b3340000)
from space 104832K, 0% used [0x00000000b99a0000, 0x00000000b9a3a2c8, 0x00000000c0000000)
to space 104832K, 0% used [0x00000000b3340000, 0x00000000b3340000, 0x00000000b99a0000)
tenured generation total 1048576K, used 614400K [0x00000000c0000000, 0x0000000100000000, 0x0000000100000000)
the space 1048576K, 58% used [0x00000000c0000000, 0x00000000e5800020, 0x00000000e5800200, 0x0000000100000000)
Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
4 . Survivor 区中相同年龄对象内存站一半多内存,则如果对象大于该年龄的对象,则直接进入老年代
-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
在执行alloc3的时候发现-Xmn1024M已经无法放入,所以触发新生代GC,由于900M是-Xmn1024M一半多,所以该对象直接进入老年代。public class Demo {
private static final int M = 1024 * 1024;
public static void main(String[] args) {
byte[] alloc1, alloc2, alloc3, alloc4;
alloc1 = new byte[100 * M];
alloc2 = new byte[100 * M];
alloc3 = new byte[900 * M];
}
}
Heap
def new generation total 943744K, used 271913K [0x0000000080000000, 0x00000000c0000000, 0x00000000c0000000)
eden space 838912K, 32% used [0x0000000080000000, 0x000000009098a598, 0x00000000b3340000)
from space 104832K, 0% used [0x00000000b3340000, 0x00000000b3340000, 0x00000000b99a0000)
to space 104832K, 0% used [0x00000000b99a0000, 0x00000000b99a0000, 0x00000000c0000000)
tenured generation total 1048576K, used 921600K [0x00000000c0000000, 0x0000000100000000, 0x0000000100000000)
the space 1048576K, 87% used [0x00000000c0000000, 0x00000000f8400010, 0x00000000f8400200, 0x0000000100000000)
Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1
如果该对象没有新生代的一半那进入老年代吗?
可以看出tenured generation total 1048576K, used 0K , 老年代使用为0 , eden区完全可以容纳这些对象。
public class Demo {
private static final int M = 1024 * 1024;
public static void main(String[] args) {
byte[] alloc1, alloc2, alloc3, alloc4;
alloc1 = new byte[100 * M];
alloc2 = new byte[100 * M];
alloc3 = new byte[500 * M];
}
}
Heap
def new generation total 943744K, used 783913K [0x0000000080000000, 0x00000000c0000000, 0x00000000c0000000)
eden space 838912K, 93% used [0x0000000080000000, 0x00000000afd8a5a8, 0x00000000b3340000)
from space 104832K, 0% used [0x00000000b3340000, 0x00000000b3340000, 0x00000000b99a0000)
to space 104832K, 0% used [0x00000000b99a0000, 0x00000000b99a0000, 0x00000000c0000000)
tenured generation total 1048576K, used 0K [0x00000000c0000000, 0x0000000100000000, 0x0000000100000000)
the space 1048576K, 0% used [0x00000000c0000000, 0x00000000c0000000, 0x00000000c0000200, 0x0000000100000000)
Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
参考文章
https://www.cnblogs.com/myseries/p/12084266.html
https://www.cnblogs.com/qianguyihao/p/4744233.html