jvm知识简介 - wtdig/study GitHub Wiki
jvm是java代码运行的虚拟系统,由类加载系统,方法区、java堆、直接内存、java栈、本地方法栈、pc寄存器、垃圾回收系统、执行引擎组成;
类加载系统将代码加载到方法区中,在堆中创建对象,在栈中执行程序;
1)类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
2)java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。
3)java的NIO库允许java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
4)垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。和C/C++不同,java中所有的对象空间释放都是隐式的,也就是说,java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。
5)每一个java虚拟机线程都有一个私有的java栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着帧信息,java栈中保存着局部变量、方法参数,同时和java方法的调用、返回密切相关。
6)本地方法栈和java栈非常类似,最大的不同在于java栈用于方法的调用,而本地方法栈则用于本地方法的调用,作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)
7)PC(Program Counter)寄存器也是每一个线程私有的空间,java虚拟机会为每一个java线程创建PC寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined
8)执行引擎是java虚拟机的最核心组件之一,它负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。
java堆是和应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆上。并且java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示的释放。
根据java回收机制的不同,java堆有可能拥有不同的结构。最为常见的一种构成是将整个java堆分为新生代和老年代。其中新生代存放新生对象或者年龄不大的对象,老年代则存放老年对象。新生代有可能分为eden区、s0区、s1区,s0区和s1区也被称为from和to区,他们是两块大小相同、可以互换角色的内存空间。
在绝大多数情况下,对象首先分配在eden区,在一次新生代回收之后,如果对象还存活,则进入s0或者s1,每经过一次新生代回收,对象如果存活,它的年龄就会加1。当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。其具体的垃圾回收算法在后面会介绍。
内存管理参数
新生代[young generation]: 存储新分配的和较年轻的对象
老生代[old generation]: 存储着长寿的对象
永久代[permanent generation]: 存储需要伴随整个JVM生命周期的对象 比如已加载的对象的类定义或者String对象内部Cache
1. -Xms 和 -Xmx (-XX:InitialHeapSize 和 -XX:MaxHeapSize)
1 ) -Xms 和 -XX:InitialHeapSize
-Xms是 -XX:InitialHeapSize的简写 表示的是初始化堆的大小
2 ) -Xmx 和 -XX:MaxHeapSize
-Xmx 是 -XX:MaxHeapSize的简写 表示的是设置堆的最大大小
需要注意的是,所有JVM关于初始\最大堆内存大小的输出都是使用它们的完整名称:“InitialHeapSize”和“InitialHeapSize”。所以当你查询一个正在运行的JVM的堆内存大小时,如使用-XX:+PrintCommandLineFlags参数或者通过JMX查询,应该寻找“InitialHeapSize”和“InitialHeapSize”标志而不是“Xms”和“Xmx”
2. -XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath
1 ) -XX:+HeapDumpOnOutOfMemoryError:
使得JVM在产生内存溢出时自动生成堆内存快照
2 ) XX:HeapDumpPath=<path>
改变默认的堆内存快照生成路径,<path>可以是相对或者绝对路径
3.-XX:OnOutOfMemoryError
当内存发生溢出时 执行一串指令
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp
4.-XX:PermSize和-XX:MaxPermSize
1 ) -XX:PermSize:设置永久代的初始大小
2 ) -XX:MaxPermSize:设置永久代的最大大小
java -XX:PermSize=128m -XX:MaxPermSize=256m MyApp
5.-XX:InitialCodeCacheSize和-XX:ReservedCodeCacheSize
代码缓存内存区域 用来存储已编译方法生成的本地代码
如果代码缓存被占满,JVM会打印出一条警告消息,并切换到interpreted-only 模式:JIT编译器被停用,字节码将不再会被编译成机器码。因此,应用程序将继续运行,但运行速度会降低一个数量级
6. -XX:+UseCodeCacheFlushing
如果代码缓存不断增长[因为热部署引起的内存泄露 提高代码的缓存大小只会延缓发生溢出]
根据的解决方法可以使用-XX:+UseCodeCacheFlushing使当代码缓存被填满时 让JVM放弃一些编译代码
GC
1.按照规定 新对象首先分配在Eden中[如果新对象过大 会直接分配在老生代中]
2.在GC中 Eden区的对象会被移动到Survivor中 直到对象满足一定的年纪[熬过GC的次数]会被移动到老生带
GC过程
新生代的GC采用复制算法,在GC前To Survivor保持清空,对象保存在Eden和From Survivor中 GC运行时,Eden中的幸存对象被复制到To Survior中,针对From Survior中的幸存对象会考虑对象的年纪,如果年纪没有达到阈值,会被复制到To Survior去,如果达到阈值会被复制到老生代。 复制阶段完成后,Eden和From Survivor中只保存死对象,可以清空,如果在复制过程中To Survior被填满 剩余的对象会被复制到生代, 最后From Survivor和To Survior会调换名字
总结:
一般对象出生在Eden区 年轻代GC过程中对象在两个幸存区之间移动 如果对象存活到指定的年龄 会被移动到老生代 对象在老生代死亡时 需要更高级别的GC 更重量级的GC算法[老生代不使用复制算法 因为老生代中没有多余的内存空间复制] 如果新生代过小 导致新生对象很快晋升到老生代 在老生代中的对象很难被回收 如果新生代过大 会发生过多的赋值过程
1) -XX:NewSize和-XX:MaxNewSize
1 ) -XX:NewSize 设置新生代的初始大小
2 ) -XX:MaxNewSize 设置新生代的最大大小
注意:新生代只是堆的一部分 新生代越大老生代越小 一般不允许新生代比老生代还大 考虑到GC最坏的情况 新生代全部复制到老生代会产生OOM错误
一般设置为-Xmx/2[堆大小的一半]
2) -XX:NewRatio
设置新生代和老生代的相对大小
优点是新生代大小会随着整个堆大小动态扩展
-XX:NewRatio=3[老生代/新生代=3]
3)-XX:SurvivorRatio
指定Eden区和Survivor区的大小比例
注意两个幸存区是一样大的
-XX:SurvivorRatio=10 表示Eden区占整个新生代的10/12 每个Survivor占1/12
Survior区过大 虽然有足够的空间容纳GC后的幸存对象 但是Eden区药效会导致空间很快耗尽 增加新生代GC次数
Survior区过小 没有足够的空间容纳GC后的幸存对象 使得对象都会移动到老生代 不方便回收
4) -XX:+PrintTenuringDistribution
指定JVM在每次新生代GC时 输出Survivor中对象的年龄分布
5) -XX:InitialTenuringThreshold,-XX:MaxTenuringThreshold和-XX:TargetSurvivorRatio
1 ) -XX:InitialTenuringThreshold 设置老生代阈值的初始值
2 ) -XX:MaxTenuringThreshold 设置老生代阈值的最大值
3 ) -XX:TargetSurvivorRatio设置幸存区的目标使用率
4 ) -XX:+NeverTenure和-XX:+AlwaysTenure
1 ) -XX:+NeverTenure:对象永远不会晋升到老年代[当不需要老生代的时候可以这样设置]
2 ) -XX:+AlwaysTenure:表示没有Survivor区 对象会直接被移动到老生代中
package com.bjsxt.base001;
public class Test01 {
public static void main(String[] args) {
//-Xms5m -Xmx20m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags
//查看GC信息
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
byte[] b1 = new byte[1*1024*1024];
System.out.println("分配了1M");
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
byte[] b2 = new byte[4*1024*1024];
System.out.println("分配了4M");
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
}
}
package com.bjsxt.base001;
public class Test02 {
public static void main(String[] args) {
//第一次配置
//-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
//第二次配置
//-Xms20m -Xmx20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
//第三次配置
//-XX:NewRatio=老年代/新生代
//-Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
byte[] b = null;
//连续向系统申请10MB空间
for(int i = 0 ; i <10; i ++){
b = new byte[1*1024*1024];
}
}
}
package com.bjsxt.base001;
import java.util.Vector;
public class Test03 {
public static void main(String[] args) {
//-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Test03.dump
//堆内存溢出
Vector v = new Vector();
for(int i=0; i < 5; i ++){
v.add(new Byte[1*1024*1024]);
}
}
}
package com.bjsxt.base001;
public class Test04 {
//-Xss1m
//-Xss5m
//栈调用深度
private static int count;
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args){
try {
recursion();
} catch (Throwable t) {
System.out.println("调用最大深入:" + count);
t.printStackTrace();
}
}
}
package com.bjsxt.base001;
import java.util.HashMap;
import java.util.Map;
public class Test05 {
public static void main(String[] args) {
//初始的对象在eden区
//参数:-Xmx64M -Xms64M -XX:+PrintGCDetails
// for(int i=0; i< 5; i++){
// byte[] b = new byte[1024*1024];
// }
//测试进入老年代的对象
//
//参数:-Xmx1024M -Xms1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails
//-XX:+PrintHeapAtGC
// Map<Integer, byte[]> m = new HashMap<Integer, byte[]>();
// for(int i =0; i <5 ; i++) {
// byte[] b = new byte[1024*1024];
// m.put(i, b);
// }
//
// for(int k = 0; k<20; k++) {
// for(int j = 0; j<300; j++){
// byte[] b = new byte[1024*1024];
// }
// }
}
}
package com.bjsxt.base001;
import java.util.HashMap;
import java.util.Map;
public class Test06 {
public static void main(String[] args) {
//参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000
//这种现象原因为:虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会
//参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB
Map<Integer, byte[]> m = new HashMap<Integer, byte[]>();
for(int i=0; i< 5*1024; i++){
byte[] b = new byte[1024];
m.put(i, b);
}
}
}
package com.bjsxt.base001;
public class Test07 {
public static void alloc(){
byte[] b = new byte[2];
}
public static void main(String[] args) {
//TLAB分配
//参数:-XX:+UseTLAB -XX:+PrintTLAB -XX:+PrintGC -XX:TLABSize=102400 -XX:-ResizeTLAB -XX:TLABRefillWasteFraction=100 -XX:-DoEscapeAnalysis -server
for(int i=0; i<10000000;i++){
alloc();
}
}
}
package com.bjsxt.entity;
public class User {
private String id ;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String name ;
}
分代算法:根据不同的分区进行不同的算法,比如:gc算法在年轻代和老年代,使用不同的算法
复制算法:用于年轻代;标记压缩算法:用于老年代
垃圾回收器:串行回收器(单线程)、并行回收器(多线程)、cms回收器(并发标记)
jvm在分配对象空间时,一般会放入新生代的eden区,如果创建的对象空间比较小的化,会存入到tlab区域(Thread location allocation buff),过大的化,直接放入老年代