一、虚拟机简介
逻辑上,可看作一台虚拟的计算机,实际上,一个软件,能够执行一系列虚拟的计算指令。
可分为系统虚拟机和软件虚拟机。
系统虚拟机:对物理计算机的仿真,如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核心技术》