JVM虚拟机详解——java

一、虚拟机简介

逻辑上,可看作一台虚拟的计算机,实际上,一个软件,能够执行一系列虚拟的计算指令。

可分为系统虚拟机和软件虚拟机。

系统虚拟机:对物理计算机的仿真,如VMWare

软件虚拟机:专门为单个计算程序而设计的,如JVM

二、Java内存分类

java自动内存管理,程序员只需要申请使用,系统会检查无用的对象并回收内存,系统统一管理内存,内存使用相对高效,但也会出现异常。

线程私有内存:

  • 程序计数器:一块小内存,每一个线程都有,存储线程正在执行的方法,方法为本地(native)时则值未定义,当前方法为非本地方法时,则包含了当前正在执行指令的地址。当前唯一一块不会引起OutOfMemoryError异常。
  • java虚拟机栈:每个线程有自己的独立java虚拟机栈。私有的。每个方法从调用到完成对应一个栈帧在栈中入栈、出栈的过程。栈帧存储局部变量表,操作数栈等。局部变量表存放方法中存在“栈”里面的东西。
  • 本地方法栈:存储native方法的执行信息,线程私有,VM规范没有对本地方法栈做出明显规定。

线程共享内存:

  • 堆:所有线程共享,最大的空间。对象实例和数组都是在堆上分配内存,垃圾回收主要区域,设置大小通过-Xms初始堆值,-Xmx最大堆值来设置。
  • 方法区:存储JVM已经加载类的结构,所有线程共享。比如运行时的常量池,类信息】常量、静态变量等。JVM启动时,逻辑上属于堆的一部分。很少做垃圾回收。
  • 运行时的常量池:Class文件中常量池的运行时表示,属于方法区的一部分。java语言并不要求常量一定只有在编译期产生。

 三、JVM内存参数

此图为eclipse2019中运行类时配置参数的图:

上面为程序参数,下面为虚拟机参数。

-X参数:不标准,不在所有的VM通用,即一定要注意jdk版本是否支持该参数;-XX参数,不稳定,容易变更,随着版本更新可能会淘汰。所以使用参数时一定要注意。

设置参数-Xmx20M则设置堆最大20M,

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
	public static void main(String[] args) {
		
		List<HeapObject> list = new ArrayList<>();

		while (true) {
			list.add(new HeapObject());
			System.out.println(list.size());
		}
		
		//System.out.println(Runtime.getRuntime().maxMemory()/1024/1024 + "M");
	}
}

class HeapObject {
}


输出:
.....
810324
810325
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
	at java.base/java.util.ArrayList.grow(ArrayList.java:237)
	at java.base/java.util.ArrayList.grow(ArrayList.java:242)
	at java.base/java.util.ArrayList.add(ArrayList.java:485)
	at java.base/java.util.ArrayList.add(ArrayList.java:498)
	at HeapOOM.main(HeapOOM.java:10)

 jvm栈:

主要存储方法,且和方法中的变量有关,所以JvmStackSOF更容易耗光内存。

 方法区:

jdk7及以前参数为:-XX:PermSize , -XX:MaxPermSize

jak8及以后就参数更改为 -XX:MetaspaceSize,-XX:MaxMetaspaceSize

四、对象引用判断无用对象

准备知识:

java语言含有内存自动管理,系统会检查无用得对象并收回内存。JVM内置了垃圾收集器用于回收。

回收时需要做到:需要判定无用得的对象,何时启动回收,并且需要不影响程序的正常运行,回收过程需要速度快时间短影响小。

java对象的生命周期:对象通过构造函数创建,但是没有析构函数回收内存。对象只能存在离它最近的一对大括号中。

java中有内存回收的API:

  • 如:Object的finalize方法,垃圾收集器在回收对象时调用,有且仅呗调用一次。备注:但是此方法不靠谱,因为无法预测什么时候被调用。
  • 如:System的gc方法,运行垃圾收集器。但是也不靠谱,还是需要虚拟机做出判断是否释放。

对象引用链:

基于对象引用判断无用对象。零引用、互引用等。

通过一系列的"GC Roots"对象作为起始点,从这些节点开始向下搜索。

利用对象引用链来判断:

“GC Roots"对象包括

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

引用的分类:

 强引用

设置-Xmx4M,运行后报错,正式内存并未释放。


public class StrongReferenceTest {

	public static void main(String[] args) {
		StringBuilder s1 = new StringBuilder();
		for(int i=0;i<10000;i++)
		{
			s1.append("00000000000000000000");
		}
		
		StringBuilder s2 = s1;
		s1 = null; //s1 为null, 但是s2依旧占据内存
		//s2 = null;
		System.gc(); 
		//垃圾回收, 无法对强类型引用回收, 内存被占用, 引发异常
		
		byte[] b = new byte[1024*1024*3];
		
	}

}
输出:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at StrongReferenceTest.main(StrongReferenceTest.java:17)

软引用:

设置-Xmx5M,运行后内存足够是则不释放,内存不够则优先释放。

import java.lang.ref.SoftReference;

public class SoftReferenceTest {

	public static void main(String[] args) {
		StringBuilder s1 = new StringBuilder();
		for(int i=0;i<100000;i++)
		{
			s1.append("0000000000");
		}
		
		SoftReference<StringBuilder> s2 = new SoftReference<StringBuilder>(s1);
		s1 = null;
		
		System.out.println(s2.get().length()); //not null
		
		System.gc();
		//软引用, 内存不紧张, 没有回收
		System.out.println(s2.get().length()); //not null
		
		byte[] b = new byte[(int)(1024*1024*3.5)];
		
		System.gc();
		//内存紧张, 软引用被回收
		System.out.println(s2.get()); //null
		
	}

}

弱引用:

WeakReference<StringBuilder> s2 = new WeakReference<StringBuilder>(s1);

虚引用:一般程序员不常用,因为不好控制。

PhantomReference<StringBuilder> s2 = new PhantomReference<StringBuilder>(s1,queue);

五、垃圾收集算法

引用计数法:

有引用加一,引用失效减一,计数器为0的对象则回收

优点:简单,高效。缺点:无法识别对象之间的循环引用

标记-清除法:

标记所有需要回收的对象,统一回收所有被标记的对象

优点:简单。缺点:效率不高,内存碎片。

复制算法:

优点:简单、高效。缺点:可用内存减少,对象存活率高时赋值操作较多。

标记-整理算法

标记需待回收的对象,整理时将所有存活的对象都向一端移动,然后直接清理端编辑以外的内存。

优点:比卖你碎片产生,无需两块相同的内存。缺点:计算代价大,标记+整理,更新引用地址。

分代收集:

一般都会采用此方法,分为新生代和老年代。

新生代:存放短暂生命周期的对象,新创建的对象都先放入新生代。

老年代:一个对象经过几次gc仍然存活则放入老年代。这些对象可以活很长时间。

新生代:采用复制算法

老年代:采用标记清除或者标记整理。

六、堆内存参数和GC跟踪。


/**
 * 来自于《实战Java虚拟机》
 * -Xms5M -Xmx20M -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
 * @author Tom
 *
 */
public class HeapAlloc {

	public static void main(String[] args) {
		
		printMemoryInfo();
		byte[] b = new byte[1*1024*1024];
		System.out.println("分配1MB空间");
		
		printMemoryInfo();
		b = new byte[4*1024*1024];
		System.out.println("分配4MB空间");

		printMemoryInfo();
	}
	
	public static void printMemoryInfo()
	{
		System.out.print("maxMemory=");
		System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024.0 + " MB");
		System.out.print("freeMemory=");
		System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024.0 + " MB");
		System.out.print("totalMemory=");
		System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024.0 + " MB");
	}

}

/**
 * 来自于《实战Java虚拟机》
 * -Xmx20m -Xms20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代1M,eden/s0=2, eden 512KB, s0=s1=256KB 
 * 新生代无法容纳1M,所以直接放老年代
 * 
 * -Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代7M,eden/s0=2, eden=3.5M, s0=s1=1.75M
 * 所以可以容纳几个数组,但是无法容纳所有,因此发生GC
 * 
 * -Xmx20m -Xms20m -Xmn15m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代15M,eden/s0=8, eden=12M, s0=s1=1.5M
 * 
 * -Xmx20m -Xms20m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代是6.6M,老年代13.3M
 * @author Tom
 *
 */
public class NewSizeDemo {

	public static void main(String[] args) {
		
		byte[] b = null;
		for(int i=0;i<10;i++)
		{
			b = new byte[1*1024*1024];
		}		
	}
}

收集器还有很多种,性能也都不一样。可以后续了解。

 参考中国大学mooc《Java核心技术》

发布了55 篇原创文章 · 获赞 17 · 访问量 4984

猜你喜欢

转载自blog.csdn.net/weixin_43698704/article/details/104504701