java:List、Map、Set默认初始容量和扩容增量及加载因子 & 内存泄漏 & 线程安全
加载因子的系数小于等于1,意指 即当 元素个数 超过 容量长度*加载因子的系数 时,进行扩容。
eg:加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
2.内存泄漏(溢出)
2.1. Q:在Java中怎么可以产生内存泄露?
A:Java中,造成内存泄露的原因有很多种。典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况。最后会生成很多重复的对象。所有的内存泄露最后都会抛出OutOfMemoryError异常,下面通过一段简短的通过无限循环模拟内存泄露的例子说明一下。
package com.test; import java.util.HashMap; import java.util.Map; import static java.lang.Thread.sleep; /** * @author riemann * @date 2019/04/21 0:03 */ public class TestHashMapMemoryLeak { public static void main(String[] args) { Map<Key, String> map = new HashMap<Key, String>(1000); int count = 0; while (true) { map.put(new Key("dummyKey"), "value"); count++; if (count % 1000 == 0) { System.out.println("map size: " + map.size()); System.out.println("Free memory after count " + count + " is " + getFreeMemory() + "MB"); sleep(100); } } } public static void sleep(long sleepTime) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } public static long getFreeMemory() { return Runtime.getRuntime().freeMemory() / (1024 * 1024); } static class Key { private String key; public Key(String key) { this.key = key; } } }
运行的时候,和那篇文中作者运行结果并不一致,后来配置了下虚拟机的参数,发现虚拟机的运行内存为2的次方,不会设置参数的读者可以参看下网络的资源。我这里给出一种IDEA的配置方法。右击项目->Run->Edit Configuration。配置JVM参数如图。
-Xms3m -Xmx3m -XX:+HeapDumpOnOutOfMemoryError
运行结果:
map size: 1000 Free memory after count 1000 is 2MB map size: 2000 Free memory after count 2000 is 2MB map size: 3000 Free memory after count 3000 is 2MB ... Free memory after count 13000 is 1MB map size: 14000 Free memory after count 14000 is 1MB map size: 15000 Free memory after count 15000 is 1MB map size: 16000 ... Free memory after count 43000 is 0MB map size: 44000 Free memory after count 44000 is 0MB map size: 45000 Free memory after count 45000 is 0MB java.lang.OutOfMemoryError: GC overhead limit exceeded Dumping heap to java_pid2732.hprof ... Exception in thread "main" Heap dump file created [5925166 bytes in 0.118 secs] java.lang.OutOfMemoryError: GC overhead limit exceeded at com.test.TestHashMapMemoryLeak.main(TestHashMapMemoryLeak.java:17)
引起错误的原因很明显,key的类没有实现hashcode()和 equals(),导致hashmap存储了大量的相同的entry。明明插入了2个Key一样的键值对,可是为什么其中个数暴涨?Fine,我们不用Key这个类来作为Key,而是使用String类来作为Key。具体如下:
Map<String, String> map = new HashMap<String, String>(1000);
再次运行,发现刚刚出现的内存泄漏问题不再出现了,原因很简单,没有覆写hashcode()和equal()方法导致每次都可以成功插入到Map中。覆写hashcode()和equal()来解决这个问题。如下:
static class Key { private String key; public Key(String key) { this.key = key; } public boolean equals(Object obj) { if (obj instanceof Key) { return key.equals(((Key) obj).key); } else { return false; } } public int hashCode() { return key.hashCode(); } }
重新执行程序会得到如下结果:
map size: 1 Free memory after count 1000 is 2MB map size: 1 Free memory after count 2000 is 2MB map size: 1 Free memory after count 3000 is 2MB ... map size: 1 Free memory after count 209000 is 2MB map size: 1 Free memory after count 210000 is 2MB map size: 1 Free memory after count 211000 is 2MB
(End:PS:有读者指出:上面所述并非内存泄露问题,这里表示赞同。此处应该是内存溢出问题。)
2.2. Q:在实际场景中,你怎么查找内存泄露?
A:通过以下代码获取线程ID :jps
C:\Users\Administrator>jps 2976 RemoteMavenServer 2116 Jps 3548 TestHashMapMemoryLeak 4108 Launcher 4140
jconsole 3548
没有内存泄露的:
造成内存泄露的:
2.1、JAVA中的垃圾收集器相对于以前的语言的优势是什么?
过去的语言要求程序员显式的分配内存、回收内存。但是这种手工的回收往往会造成“内存泄漏”(有过C++开发经验的读者应该清楚这个),即由于某种原因使分配的内存始终没有得到释放。如果该任务不断的被重复执行,将会导致大量的内存得不到释放,后果很明显。相比之下,JAVA提供了垃圾收集器来回收内存,避免了很多潜在的危险。JAVA在创建对象时会自动分配内存,并当该对象的引用不存在时释放这块内存。
JAVA中使用被称为垃圾收集器的技术来监视JAVA程序的运行,当对象不再使用时,就自动释放对象所使用的内存。JAVA使用一系列软指针来跟踪对象的各个引用,并用一个对象表将这些软指针映射为对象的引用。之所以称为软指针,是因为这些指针并不直接指向对象,而是指向对象的引用。使用软指针,JAVA的垃圾收集器能够以单独的线程在后台运行,并依次检查每个对象。通过更改对象表项,垃圾收集器可以标记对象、移除对象、移动对象或检查对象。
垃圾收集器是自动运行的,一般情况下,无须显式地请求垃圾收集器。程序运行时,垃圾收集器会不时检查对象的各个引用,并回收无引用对象所占用的内存。调用System类中的静态gc()方法可以运行垃圾收集器,但这些并不能保证立即回收指定对象。
2.2、JAVA是如何管理内存的?
JAVA的内存管理就是对象的分配和释放问题。在JAVA中,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在JAVA中,内存的分配是由程序完成的,而内存的释放是由GC完成,这种收支两条线的方法确实简化了一定的工作。但同时,也加重了JVM的工作。这就是JAVA程序运行速度较慢的原因之一。因为GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。
为了更好的理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引用对象。另外,每个线程对象可以作为一个图的起始顶点,例如,大多程序从main进程开始执行,那么该图就是以main进行顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象(连通子图)对象不再被引用,可以被GC回收。
用有向图表示内存管理。对象程序的每一个时刻,都有一个有向图表示JVM的内存分配情况。下图就是左边程序运行到第六行的示意图。
我们来看一个例子,观察如下代码,判定对象是否被回收。
Vector v = new Vector(); for (int i = 0; i < 100; i++) { Object o = new Object(); v.add(o); o = null; }
我们发现,虽然o=null了,可是依旧没有被回收(原因很明显,该对象依旧被引用),这也正是JAVA中同样存在内存泄漏的原因所在!下面我们来解释下什么是JAVA中的内存泄漏。
2.3、什么是JAVA中的内存泄漏?
在JAVA中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:(1)对象是可达的,即在有向图中,存在有向路可以与其相连;(2)对象是无用的,即程序以后不会再使用这些对象。
如果对象满足上面的(1)(2)条件,这些对象就可以判定为JAVA中的内存泄漏,这些对象不会被GC所回收,然而他们却占用内存。
在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在JAVA中,这些不可达的对象都由GC负责回收,因此,程序员不需要考虑这部分内存泄漏。对于C++,程序员需要自己管理边和顶点,而对于JAVA程序员,只需要管理边就可以了,通过这种方式,JAVA提高了编程效率。
对于程序员而言,GC基本是透明的、不可见的。虽然我们只有几个函数可以访问GC,例如,运行GC的函数System.gc(),但是根据JAVA语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能不同的算法管理GC。通常,GC线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中段式执行GC。但通常来说,我们不需要关心这些,除非在一些特定的场合,GC的执行影响应用程序的性能,例如,对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序的执行而进行垃圾回收,那么需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如,将垃圾回收分解为一系列的小步骤执行。Sun提供的HotSpotJVM就支持这一特性。
摘自:https://blog.csdn.net/riemann_/article/details/89426634
垃圾回收机制详见:https://editor.csdn.net/md/?articleId=113408101
3.线程安全
详见 https://www.cnblogs.com/jmcui/p/11422083.html