JVM内存模型
首先上图
主要概念
堆、栈、本地方法栈、方法区、程序计数器、直接内存、常量池
堆
Java堆是程序员需要重点关注的一块区域,因为涉及到内存的分配(new关键字,反射等)与回收(回收算法,收集器等);
对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。
-
new出来的对象都存在堆里面
-
所有线程共享
-
Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
-
其他地方的对象语言都是存放地址来指向此处
堆分为年轻代(1/3)、老年代(2/3);年轻代又分为Eden(4/5)区和Survior(1/5)区;Survior区被一分为二,From Survivor 空间、To Survivor 空间,用于GC的复制。
栈
线程私有,生命周期和线程,每个方法在执行的同时都会创建一个 栈帧用于 存储局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用;
-
栈帧,一个线程被划分成许多的栈帧,每一个方法都是一个栈帧(如main函数栈帧、run函数栈帧)
-
局部变量表,方法内部的int a=0;User user= new User();这种局部变量(或者地址引用)会被保存在这个地方。(其实远没有这么简单,准确来说,javac编译后的class文件,会将这些变量定义转换成另外一种存储,简单来理解是正确的)
-
操作数栈,顾名思义,就是一种栈的数据结构类型,当程序运行时,会将代码中用到的变量存入到这个栈中,如int a = b+c;会先将b和c入栈(先从局部变量表中取出),然后调用加法(自动出栈两个元素相加),结果入栈,在局部变量表定义一个新变量a存放出栈的结果
-
动态链接, 被调用的目标方法在编译期无法被确定下来,只能够在程序运行期将方法的符号引用转换为直接引用,这种引用转换的过程具备动态性,称为动态链接。方法的绑定机制分为早期绑定(Early Binding)和晚期绑定(Late Bingind)。绑定是一个字段、方法或类在符号引用被替换为直接引用的过程。早期绑定: 被调用的目标方法在编译期可知,且运行保持不变。晚期绑定: 被调用方法在编译期无法被确定下来,只能够在程序运行期根据实际类型绑定相关的方法。
-
静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的****目标方法在编译期可知,且运行期保持不变。将调用方法的符号引用转换为直接引用的过程称为静态链接。
-
举个例子,静态方法就是使用静态链接,而多态机制,即动态调用子类的方法就是动态机制。除了被static修饰的静态方法,被final修饰的禁止子类覆盖的方法(invokevirtual),所有类的初始化方法(invokespecial),所有被private修饰的私有方法(invokespecial) ,jvm采用静态绑定机制来调用这些方法。
-
方法出口:指示这个方法运行完了会返回至哪一个地方
本地方法栈
线程私有,本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧, JVM只是简单地动态链接并直接调用native方法;(很多java的底层静态方法都是调用C语言写的方法)
方法区
线程共享,也叫永久区(本质上两者并不等价),用于存储已经被虚拟机加载的类信息,常量(“zdy”,"123"等),静态变量(static变量)等数据。 (jdk1.8已经将方法区去掉了,将方法区移动到直接内存)。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
JDK1.8直接 将方法区去掉,在本地内存中新增 元数据空间。运行时常量池仍然在堆中。元数据区存放类加载信息。
运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面(“zdy”,"123"等)和符号引用。
jDK1.7将运行时常量池从方法区移除到堆内存(也就是局部变量表)。
直接内存
不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;
1)如果使用了NIO(New Input/Output)(引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存),这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;
2) 这块内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;
程序计数器
线程私有,较小的内存空间, 当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响;
当线程被挂起(失去时间片、被其他线程抢占cpu时),为了避免从头开始运行方法,就使用程序计数器来恢复到之前运行到的地方
线程私有和线程共享
多线程共享内存区域: 方法区、堆。
每一个线程独享内存: java栈、本地方法栈、程序计数器。
线程安全
线程安全本质是由于多个线程对同一个堆内存中的Count变量操作的时候,每一个线程会在线程内部创建这个堆内存Count变量的副本,线程内所有的操作都是对这个Count副本进行操作。这时如果其他线程操作这个堆内存Count变量,改变了Count值对这个线程是不可见的。当前线程操作完Count变量将值从副本空间写到主内存(堆内存)的时候就会覆盖其他线程操作Count变量的结果,引发线程不安全问题。
GC流程
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
从GC Roots根节点开始往下找它所引用的,这些就是存活的变量,不会被清理,其他都会被GC回收(每次GC都会停掉其他线程,专门开一个线程来执行GC)
- 首先第一次 From Survivor 空间、To Survivor 空间都为空,此时Eden区满了,触发GC(minor gc),寻找GC Roots根节点能达到的对象,将其他对象清除调,剩余的对象放置From Survivor空间。此时第一次GC完成后,Eden:0,From Survivor:存活的对象,To Survivor:0。
- 每经历一次GC活下来的对象,对象头中的分代年龄都会+1
- Eden又满了,又触发GC(minor gc),此时由于一个 Survivor空间(From Survivor空间)有对象存活,所以会在Eden区和From Survivor空间中进行GC,将活下来的对象全部移动到To Survivor 空间。此时第二次GC完成后,Eden:0,From Survivor:0,To Survivor:存活的对象。
- 分代年龄都会+1
- 重复上述过程(From Survivor空间和To Survivor相互交换)(复制算法)
- 当对象的分代年龄都达到15时,这时GC就会认为这是个“老不死”的对象,就会将其移动到老年代(常见的比如spring的各种bean)
- 当老年代也放满了,触发触发GC(full gc)(清理堆中所有的。也会清理方法区)
jvisualvm:查看jvm的内存情况。
jVM常数配置
-Xms 初始堆内存大小
-Xmx 最大堆内存大小
-Xss 单个线程栈大小
-XX:NewSize 初始新生代堆大小
-XX:MaxNewSize 生代最大堆大小
-XX:PermSize 方法区初始大小(JDK1.7及以前)
-XX:MaxPermSize 方法区最大大小(JDK1.7及以前)
-XX:MetaspaceSize 元数据区初始值(JDK1.8)
-XX:MaxMetaspaceSize 元数据区最大值(JDK1.8)
参数设置示例
jdk1.7 windows设置tomcat的catalina.bat
set JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:PermSize=128m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
jdk1.8 windows设置tomcat的catalina.bat
set JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
jdk1.7 linux设置tomcat的catalina.sh
JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:PermSize=128m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
jdk1.8 linux设置tomcat的catalina.sh
JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m