jvm知识简介 - wtdig/study GitHub Wiki

jvm知识简介

一、jvm简介、组成

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堆,分为新生代和老年代;新生代有eden区和from-to区,老年代有永久区;

java堆是和应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆上。并且java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示的释放。

根据java回收机制的不同,java堆有可能拥有不同的结构。最为常见的一种构成是将整个java堆分为新生代和老年代。其中新生代存放新生对象或者年龄不大的对象,老年代则存放老年对象。新生代有可能分为eden区、s0区、s1区,s0区和s1区也被称为from和to区,他们是两块大小相同、可以互换角色的内存空间。

在绝大多数情况下,对象首先分配在eden区,在一次新生代回收之后,如果对象还存活,则进入s0或者s1,每经过一次新生代回收,对象如果存活,它的年龄就会加1。当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。其具体的垃圾回收算法在后面会介绍。

二、jvm参数解析

参考资料

内存管理参数

新生代[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区 对象会直接被移动到老生代中

三、jvm参数演示

1)

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());
		
	}
	
}

2)

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];
		}
	}
}

3)

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]);
		}
		
	}
}

4)

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();
		}
	}
}

5)

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]; 
//			}
//		}
		
	
	}
}

6)

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);
		}
	}
}

7)

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();
		}
		
		
		
		
		
		
		
		
		
		
	}
}

8)实体类

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算法:

分代算法:根据不同的分区进行不同的算法,比如:gc算法在年轻代和老年代,使用不同的算法

复制算法:用于年轻代;标记压缩算法:用于老年代

垃圾回收器:串行回收器(单线程)、并行回收器(多线程)、cms回收器(并发标记)

jvm在分配对象空间时,一般会放入新生代的eden区,如果创建的对象空间比较小的化,会存入到tlab区域(Thread location allocation buff),过大的化,直接放入老年代

⚠️ **GitHub.com Fallback** ⚠️