查看之前的博客可以点击顶部的【分类专栏】
Java 内存结构
知识点1:Java堆(Java Heap)
Java堆是 Java 虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。Java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”(Garbage Collected Heap)。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError(内存溢出错误)的异常。
知识点2:Java虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。java虚拟机也是线程私有的,它的生命周期和线程相同。局部变量表所需的内存空间在编译期间完成分配。在运行期间不会改变局部变量表的大小。
知识点3:方法区(Method Area)
方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
垃圾回收机制
垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理。
算法:不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收,垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc() 方法来"建议"执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。
finalize方法作用
Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。
分代算法
新生代(Young):新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。
老年代(Old):老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。
JVM参数配置
常见参数配置
-XX:+PrintGC 每次触发GC的时候打印相关日志
-XX:+PrintGCDetails 更详细的GC日志
-XX:+UseSerialGC 串行回收(用得少)
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
-XX:NewRatio 配置新生代与老年代占比 1:2
含以-XX:SurvivorRatio=eden/from=den/to
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。如果不相等的情况下,GC 不停地在回收,不停地去申请内存,对性能不好。
1、堆内存大小的配置(-Xms 和 -Xmx)
性能调优主要针对堆内存。因为栈内存会自动释放,方法区满了之后垃圾回收器也会自动回收。
比如下面的代码是默认值,没有调参数之前:
private static void t7() {
System.out.print("最大内存");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("可用内存");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("已经使用内存");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
}
输出:
最大内存1794.0M
可用内存119.0960693359375M
已经使用内存123.0M
本人电脑内存:
一般来说,如果电脑是16G内存,最大内存默认是4G。如果电脑是8G,那么最大内存默认是2G。
OK,我们来调一下参数。
在 IDEA 编辑器,怎么调整 JVM 参数呢?有2个入口
1、Help 菜单下找到【Edit Custom VM Options..】这个是全局的 JVM 参数调整。调整后需要重启 IDEA。
2、某个应用的 JVM 参数调整,在【RUn】->【Edit Configurations..】
然后选择想要配置的应用,在 VM options.. 输入参数即可。比如:堆初始值我们设置5m,最大值20m:
然后重新测试:有误差。
最大内存18.0M
可用内存4.475975036621094M
已经使用内存5.5M
我们增加以下的代码:
byte[] b = new byte[6 * 1024 * 1024];
System.out.println("分配了6M空间给数组");
运行测试:
分配了6M空间给数组
最大内存18.0M
可用内存5.852684020996094M
已经使用内存13.0M
然后我们继续修改,把分配给数组的 6M 改为 25M。继续测试:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
导致了内存溢出错误。
2、设置新生代比例参数
调整参数如下:
-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
参数说明:
堆内存初始化值20m,堆内存最大值20m,新生代最大值可用1m,eden空间和from/to空间的比例为2/1
测试代码:
private static void t8() {
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
}
运行结果:
说明 GC 在不断的回收垃圾,因为我们分配的初始化内存不足。
因此我们的配置原则是:
eden空间和from/to空间的比例为2/1
新生代和老年代的占比为1/2
让垃圾回收器在新生代多回收,在老年代少回收。
Java 堆内存溢出 OutOfMemoryError
我们配置 JVM 参数如下:
-Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
测试代码如下:
private static void t8() {
List<Object> listObject = new ArrayList<>();
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
Byte[] bytes = new Byte[1 * 1024 * 1024];
listObject.add(bytes);
}
System.out.println("添加成功...");
}
测试:只输出了前面2个,就报内存溢出的错误了。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
因此,JVM 参数调整不好,也是导致内存溢出的一个原因。
Java 虚拟机栈溢出 StackOverflowError
栈溢出产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。
解决办法:设置线程最大调用深度
栈溢出示例代码,首先设置最大调用深度是 5m
-Xss5m
然后执行下面的代码:
private static void test(int count){
try {
count++;
test(count);
} catch (Throwable e) {
System.out.println("最大深度:"+count);
e.printStackTrace();
}
}
输出:
内存溢出与内存泄漏区别
Java内存泄漏就是没有及时清理内存垃圾,导致系统无法再给你提供内存资源(内存资源耗尽);
而Java内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存溢出,这个好理解,说明存储空间不够大。就像倒水倒多了,从杯子上面溢出了来了一样。
内存泄漏,原理是,使用过的内存空间没有被及时释放,长时间占用内存,最终导致内存空间不足,而出现内存溢出。
垃圾收集器
串行与并行收集器:
串行回收: JDK1.5前的默认算法。缺点是只有一个线程,执行垃圾回收时程序停止的时间比较长。通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
并行回收:多个线程执行垃圾回收适合于吞吐量的系统,回收时系统会停止运行。ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行。
参数控制:-XX:+UseParNewGC 使用ParNew收集器
-XX:ParallelGCThreads 限制线程数量
调优总结
1、垃圾回收次数和设置最大堆内存大小无关,只和初始内存有关系。初始内存会影响吞吐量。
2、初始堆值和最大堆内存内存越大,吞吐量就越高。
3、最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。
4、设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。
5、减少GC对老年代的回收。