经常忘记一些JAVA基础,每天写一点点,复习一下要点。
按顺序,知识点有:
1.对象加载的步骤?
2.句柄和直接引用
3.双亲委派机制
4.volatile关键字含义和作用 单例模式中的作用
5.static final关键字作用
6.JAVA集合 常问的是hashmap hashtable concurrentHashmap
7.String 常量池
8.JVM结构 几种概念请叙述出来?
9.GC 垃圾回收机制 4种回收算法 CMS G1垃圾回收器?
10.java内存模型
11.4种引用类型
12.内存泄漏举例 叙述
对象加载的5个步骤
- 类加载检查。当使用new 生成一个对象时,会首先去检查常量池中有没有对这个对象的引用。如果没有,再检查这个对象是否已经加载、解析、初始化过,如果没有,会进行类加载。
- 分配内存。在JAVA堆中为对象分配内存。内存分配机制有两种,第一种是指针碰撞,第二种是空闲列表。分配方式由JAVA堆是否规整决定,JAVA堆是否规整又由所采用的垃圾回收期是否有压缩整理功能决定。
- 初始化零值。为内存中已经分配的对象数据初始化零值,这样对象可以不赋初始值直接使用。
- 分配对象头。对象头包括类元数据、对象的哈希码以及对象的GC分代年龄等。
- init初始化。按照程序员的意愿对类中的数据进行初始化。
对象的内存布局
- 对象头。包括对象的哈希码、GC分代年龄等。
- 实例数据。程序中所定义的各种字段内容。
- 对齐填充。由于对象占位必须是8字节的整数倍,所以当对象实例部分空间未占满时(对象头本身是8字节的1倍或2倍),会对剩余空间进行填充。
句柄和直接引用
- 句柄。指针的指针。JAVA堆中分配句柄池,存储实例对象的地址。
优点:一个对象被多个变量引用,那么只需要更改句柄池一个。当对象地址改变时,只需改变句柄池内容即可。
缺点:速度慢。
2. 直接指针。指针,直接保存对象地址。速度快,省去了句柄池还得再次寻找地址。
优点:速度快。
缺点:对象被移动时,所有指向该对象的reference都需要被改变,耗时。
双亲委派机制
当要加载某个类时,需要把任务委托给上级类加载器,递归询问是否已经加载此类,如果没有,自己才会加载。https://www.jianshu.com/p/1e4011617650 知识点参考后整理。
- 启动类加载器(BootstrapClassLoader)
C++
编写
加载java
核心库 java.*
,构造ExtClassLoader
和AppClassLoader
。涉及到虚拟机本地实现细节,开发者无法获取到启动类加载器的引用,不允许直接通过引用进行操作。
2. 标准扩展类加载器(ExtClassLoader)java
编写
加载扩展库如classpath
中的jre
,javax.*
或者java.ext.dir
指定位置中的类,开发者可使用标准扩展类加载器。
3. 系统类加载器 (AppClassLoader)java
编写
加载程序所在的目录,如user.dir
所在的位置的class
。
4. 用户自定义类加载器(CustomClassLoader)java
编写
用户自定义的类加载器,可加载指定路径的class
文件
作用:
1.防止重复加载同一个类。
2.防止代码被篡改。(只能有一个被加载的类,已经加载的类不能被篡改)。
volatile关键字
1.保持可见性。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。(强制重读)
可见性不能保证线程安全。线程安全的三个条件:原子性、可见性、禁止重排序。三者的解释可参考
2.禁止指令重排序。
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
如果语句3是带有volatile关键字的,位于语句3前面的语句不能重排序至其后面,语句3后面的语句不能重排序到3前面。语句3前面的语句可以随便重排序,只要自己不要越过语句3就行。
经典问题:
public volatile int inc = 0;
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
public void increase() {
inc++;
}
运行结果小于10000。原因:
假如某个时刻变量inc的值为10,
线程1先读取了变量inc的原始值,线程1对变量进行自增操作,线程2此时对变量进行自增操作并写入主存,然后线程1被阻塞了;
i++操作可以被拆分为三步:
1,线程读取i的值
2、i进行自增计算
3、刷新回i的值
此时在线程1的工作内存中inc的值更新为11(volatile强制重读),然后线程1接着进行加1操作,但是线程1之前卡在了自增计算(非原子性操作,已经读到了之前的10,操作的还是10自增)所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存(11替换强制更新的11),最后写入主存。
那么两个线程分别进行了一次自增操作后,inc只增加了1,结果都为11。
- 单例模式中的volatile
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
} // 8
} // 9
return instance; // 10
} // 11
}
第7行实际上编译后分为3步:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
编译期会做指令重排序,所以会有:
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
当A线程上面132的顺序分配了对象,但是当A刚刚执行完毕3,B看到了instance已经不为null
,B此时调用了空对象,发生错误。
hapens-before原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
static 关键字
java中不存在“全局变量”概念,可以用static表示“伪全局”概念。
被static修饰就是静态变量,所以JVM肯定将其放在单独的常量池里。JVM可以方便访问它们。
要点:
- 被 static 修饰的成员变量和成员方法是独立于该类的。
静态变量和静态方法是随着类加载时被完成初始化的(类加载时已经存在),它在内存中仅有一个,且 JVM 也只会为它分配一次内存,同时类所有的实例都共享静态变量,可以直接通过类名来访问它。
可如下直接使用:
ClassName.propertyName
ClassName.methodName(……)
- 它只能调用 static 变量和方法。
- 不能以任何形式引用 this、super。
- static 变量在定义时必须要进行初始化,且初始化时间要早于非静态变量。
final关键字
某些数据不可更改。final修饰的叫常量。“常量”的使用有以下两个地方:
- 编译期常量,永远不可改变。
- 运行期初始化时,不可改变。根据对象的不同而表现不同,但同时又不希望它被改变,这个时候我们就可以使用运行期常量。
private final String final_01 = "demo";//编译期常量,必须要进行初始化,且不可更改
public class test{
public final String str;
private static Random random = new Random();
private final int final_03 = random.nextInt(50); //使用随机数来进行初始化,运行期初始化,每次new新的类都会改变内容(random)。
test(String s){
this.str=s;
}
}
注意点:
- final修饰的方法不能被修改重写,可以被继承。
- final修饰的类不能修改不能继承,最终类。
- 匿名内部类中参数必须为final。
java集合
1.Collection
最基本的集合接口,它不提供直接的实现,JAVA SDK提供的类都是继承自Collection的子接口。所有实现Collection的接口类必须实现两类构造函数:
a) 无参构造函数。创建空的Collection。
b) 有参构造函数,用于创建新的Collection。
List
a) ArrayList
初始容量10。基于数组实现。每次扩容1.5倍。
b) LinkedList
基于双向链表实现,近开头或结尾(靠近索引)遍历链表。
c) Vector
与ArrayList一样,不同在于它是线程安全。
d) Stack
继承自Vector,后进先出的栈。提供push pop peek empty search。
Set
与list区别是不包含重复元素。
a)EnumSet
枚举专用Set,所有元素都是枚举类型。
b)HashSet
查询速度最快的集合,内部以Hashcode实现,顺序也以哈希码排序。
c)TreeSet
排序状态的Set,以TreeMap实现,以自然元素顺序或用户自定义Comparator排序。
Queue
(1)不阻塞的: PriorityQueue 和 ConcurrentLinkedQueue
PriorityQueue 和 ConcurrentLinkedQueue 类在 Collection Framework 中加入两个具体集合实现。
a)PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的
天然排序(通过其 java.util.Comparable 实现)或者根据传递给构造
函数的 java.util.Comparator 实现来定位。
b)ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。
因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinkedQueue
对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。
(2)阻塞的:
五个阻塞队列类。它实质上就是一种带有一点扭曲的 FIFO 数据结构。不是立即从队列中添加或者删除元素,线程执行操作阻塞,直到有空间或者元素可用。
五个队列所提供的各有不同:
a)ArrayBlockingQueue :一个由数组支持的有界队列。
b)LinkedBlockingQueue :一个由链接节点支持的可选有界队列。
c)PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
d)DelayQueue :一个由优先级堆支持的、基于时间的调度队列。
e)SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。
2.Map
与List和set不同,由一对键值对组成的集合,key到value的映射。
a) HashMap
- 链表+数组.内部定义hash表数组
Entry[] table
,元素通过哈希函数的到哈希地址转为数组存放索引,当有冲突同一个索引处保存链表。 - 初始大小16,容量必须为(
h & (length-1)
这条规律必须是2的n次方)。JDK1.8 链表升级为红黑树结构。当本索引冲突大于8链表就转为红黑树,当冲突值小于6再转为普通链表。 - 高位右移动是为了扰动,防止哈希冲突。在对数组长度进行按位与运算后得到的结果相同,就发生了冲突。h >>> 16过扰动计算之后,最终得到的index的值不一样了。
哈希冲突
更多细节参考其他博客,写的比较详细,记住常问的就行
b)HashTable
- 线程安全,
(hash & 0x7FFFFFFF) % tab.length
它是通过这句话直接对hash地址取模,不是像HashMap按位与。0x7FFFFFFF做一次按位与操作,主要是为了保证得到的index的第一位为0,也就是为了得到一个正数。因为有符号数第一位0代表正数,1代表负数。 - 初始大小为11,之后每次扩充为原来的2n+1。HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。
- 当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。
- 和HashMap区别
1)继承的父类不同
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
2)线程安全性不同
如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。
c) ConcurrentHashMap
- ConcurrentHashMap的hash实现和HashMap一样,但是用了不同的哈希算法(Wang/Jenkins 哈希算法)。
- 线程安全 “分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable。ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中。
- 通过把整个Map分为N个Segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
ConcurrentHashMap原理可参考
有没有
goto
语句
有,也有const
。JAVA只保留但基本不用了。
一个
.class
文件可不可以有多个类
可以有多个,但是public只有一个。两个以上public就不知道加载哪个类,会报错。
字符串池
字符串常量池底层是用HashTable实现的。
- 创建对象的两种方式。
String s1 = "hello";
String s2 = new String("hello"); System.out.println(s1==s2);//false
//第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象。
2. JDK版本中(1.7后), 字符串常量池被实现在Java堆内存中。
String str1 = "abc";
String str2 = "abc";
String str3 = "abc";
String str4 = new String("abc");
String str5 = new String("abc");
System.out.println(s1==s2);//false
3.“双引号""声明字符串的方式,
JVM首先会去字符串池中查找是否存在"abc"这个对象:
- 不存在,在字符串池中创建"abc"这个对象,将"abc"这个对象的引用地址返回给字符串常量str1。
- 如果存在,直接将池中"abc"这个对象的地址返回,赋给字符串常量。
4. new字符串
JVM首先在字符串池中查找有没有"abc"这个字符串对象:
- 存在,直接在堆中创建一个"abc"字符串对象,然后将堆中的"abc"对象的地址返回赋给引用str4。
- 不存在,字符串池中创建一个"abc"字符串对象,然后再在堆中创建一个"abc"字符串对象,将堆中"abc"字符串对象的地址返回赋给变量引用。
5. intern()
调用 intern方法时,查字符串池中:
- 与此对象字符串内容相同,如果池已经包含一个等于此String对象的字符串(用equals(object)方法确定),则返回池中的字符串。
- 不相同,将此String对象添加到池中,并返回此String对象的引用。 对于任意两个字符串s和t,所有字面值字符串和字符串赋值常量表达式都使用 intern()进行操作。
String str1 = "abc";
String str2 = new StringBuilder("ab").apend("c").toString();
String str3 = str2.intern();
System.out.println(str1==str2);//false
System.out.println(str1==str3);//true
JVM结构
JDK 1.7
jdk1.8
程序计数器
- 程序计数器 线程私有
- 概念:记录当前线程执行的指令字节码地址。
- 功能:改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
- 唯一一个不会出现
OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
2. 虚拟机栈 线程私有
- Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
- 局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
- Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。OutOfMemoryError:
- Java方法有两种返回方式:
- return 语句。
- 抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
- i++ 和 ++i 的区别:
- i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
- ++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。
本地方法栈
3. 本地方法栈 线程私有
- 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
- 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
- 也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
java堆
4. 堆 线程共享
- 存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
- 主要记垃圾回收机制。
方法区
5. 方法区 线程共享
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 别名叫做 Non-Heap(非堆)。
运行时常量池
6. 运行时常量池 线程共享
- 是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 运行期间也可能将新的常量放入池中,例如 String 类的 intern() 方法。
直接内存
7. 直接内存 线程共享
- 不是 Java 虚拟机规范中定义的内存区域。
- 受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。
元空间
8. 元空间 线程共享
- 使用本地内存(直接内存)来存储类元数据信息并称之为:元空间(Metaspace)
- java8中移除了永久代(就是方法区),新增元空间,这两者之间存储的内容(常量池,类信息,还有class的static变量)几乎没怎么变化,而是在内存限制、垃圾回收等机制上改变较大。元空间的出现就是为了解决突出的类和类加载器元数据过多导致的内存溢出问题。
- 使用JDK1.7运行Java程序,监控并耗尽默认设定的85MB大小的PermGen(1.7之前是PermGen,1.8改为元空间)内存空间。
- 使用JDK1.8运行Java程序,监控新Metaspace内存空间的动态增长和垃圾回收过程。
- 使用JDK1.8运行Java程序,模拟耗尽通过“MaxMetaspaceSize”参数设定的128MB大小的Metaspace内存空间。
垃圾回收机制
判定哪些对象是垃圾
- 判定哪些对象是垃圾
- 引用计数法
每个对象都分配一个引用计数器,用来存储该对象被引用的个数。当有地方引用它,计数器加1。当个数为0,则可以回收。
此方法有一个缺陷:两个对象互相引用,计数器永远不为0。所以JAVA没采用此方法。
- 可达性分析
把所有引用对象抽象成一棵树,从树GC Roots根结点出发,遍历所有树枝。找到的就是可达,判定存活,不能找到的就是可回收对象。
- GC Roots的种类:
虚拟机栈中引用的对象。
方法区中静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。
垃圾回收算法
2.垃圾回收算法
- 标记-清理
1)标记,利用可达性分析,找到垃圾对象。
优点:简单快捷。
缺点:产生内存碎片。
- 标记-整理
标记清理会产生内存碎片所以:
1)可达性分析找到垃圾对象和存活对象。
2)把所有存活对象堆到同一个地方,没有内存碎片。
适合存活对象多,垃圾少的情况。
- 复制
将内存划分为大小相等的两块,每次使用其中一块。这一块用完了,将活着的队形复制到另一块,然后把使用过的内存空间一次性清理掉。
优点:简单、不会产生碎片
缺点:内存利用率很低,只用了一半。
- 分代回收算法
java堆分为刚刚创建的对象、存活了一段时间的对象和永久存在的对象。
新生代与老年代的比例为1:2。
设置两个 Survivor 区解决内存碎片化。Survivor 如果只有一个区域,Minor GC 执行后,Eden 区被清空了,存活的对象放到了 S1(from) 区,而之前 S1 区中的对象,可能也有一些是需要被清除的。这时候我们怎么清除它们?在这种场景下,我们只能标记清除,标记清除最大的问题就是内存碎片。有了s2(to),将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责换一下,这时候会将 Eden 区和 s2 区中的存活对象再复制到 s1 区域,以此反复。
新生代-复制 回收机制:
区域大小比例Eden:s1:s2= 8: 1:1(Hotspot虚拟机这样划分)。新生代每次都有大量对象死亡,只有少量存活。因此采用复制算法,回收时GC把少量存活对象复制过去即可。
只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
工作原理:
1)Eden区域快满了,触发垃圾回收机制(Young GC)。此轮存活对象放入From(s1)。
2)Eden再次满,触发垃圾回收机制(Young GC),回收Eden和From(s1),存活放入To(s2)。此时s1空了,下一次回收时s1和s2职责对换,下一次Eden满时将Eden和To(s2)的存活对象放入From(s1), 如此往复。多次后一些对象在s1和s2之间多次复制,复制次数超过某个阈值(16)后,把存活对象复制到Old区域。
3)当某个s区域不足以存放存活对象,将多余对象放到Old区域。
4)Old区域满了,触发垃圾回收机制(Full GC),进行整个堆的垃圾回收(老年代也要回收了,用老年代自己的回收算法),跳到1)进行新一轮垃圾回收算法。
老年代-标记整理 回收机制:
老年代存活对象多、垃圾少。老年代仅仅通过少量地移动存活对象就能清理垃圾。
Full GC
1)年老代(Tenured)被写满
2)持久代(Perm)被写满
3)System.gc()被显示调用
4)上一次GC之后Heap的各域分配策略动态变化
垃圾回收器
3.垃圾回收器
- 吞吐量
CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。比如说虚拟机总运行了 100 分钟,用户代码时间 99 分钟,垃圾回收 时间 1 分钟,那么吞吐量就是 99%。
- 停顿时间
停顿时间 指垃圾回收器正在运行时,应用程序 的 暂停时间。
- GC的名词
新生代GC:Minor GC
老年代GC:Major GC
- 并发与并行
(1)串行(Parallel)
垃圾回收线程 进行垃圾回收工作,但此时 用户线程 仍然处于 等待状态。
(2)并发(Concurrent)
这里的并发指 用户线程 与 垃圾回收线程 交替执行。
(3)并行(Parallel)
这里的并行指 用户线程 和多条 垃圾回收线程 分别在不同 CPU 上同时工作。
回收器种类:
1)Serial(单线程)
Serial 回收器是最基本的 新生代垃圾回收器,是单线程的垃圾回收器。采用的是 复制算法。垃圾清理时,Serial回收器不存在线程间的切换,因此,在单 CPU的环境下,垃圾清除效率比较高。
2)Serial Old(单线程)
Serial Old回收器是 Serial回收器的老生代版本,单线程回收器,使用 标记-整理算法。在 JDK1.5 及其以前,它常与Parallel Scavenge回收器配合使用,达到较好的吞吐量,另外它也是 CMS 回收器在Concurrent Mode Failure时的后备方案。
3)ParNew(多线程)
ParNew回收器是在Serial回收器的基础上演化而来的,属于Serial回收器的多线程版本,采用复制算法。运行在新生代区域。在实现上,两者共用很多代码。在不同运行环境下,根据CPU核数,开启不同的线程数,从而达到最优的垃圾回收效果。
4)Parallel Scavenge(多线程)
运行在新生代区域,属于多线程的回收器,采用复制算法。与ParNew不同的是,ParNew回收器是通过控制垃圾回收的线程数来进行参数调整,而Parallel Scavenge回收器更关心的是程序运行的吞吐量。即一段时间内用户代码运行时间占总运行时间的百分比。
5)Parallel Old(多线程)
Parallel Old回收器是Parallel Scavenge回收器的老生代版本,属于多线程回收器,采用标记-整理算法。Parallel Old回收器和Parallel Scavenge回收器同样考虑了吞吐量优先这一指标,非常适合那些注重吞吐量和CPU资源敏感的场合。
6)CMS(多线程回收)
CMS回收器是回收老年代收集器。在最短回收停顿时间为前提的回收器,属于多线程回收器,采用标记-清除算法。
初始标记 :标记GC Roots能直接关联到的对象,需要在safepoint位置暂停所有执行线程。
并发标记 :进行GC Roots Tracing,遍历完从root可达的所有对象。该阶段与工作线程并发执行。
重新标记 :修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录。需要在safepoint位置暂停所有执行线程。
并发清理 :内存回收阶段,将死亡的内存对象占用的空间增加到一个空闲列表(free list),供以后的分配使用。
重置 :清理数据结构,为下一个并发收集做准备。
7)G1回收器
G1是 JDK 1.7中正式投入使用的用于取代CMS的压缩回收器。基于标记整理的垃圾回收器。年轻代分为Eden和Survivor两个区,老年代分为Old和Humongous(大对象,占用的空间超过了分区容量50%)两个区。
G1首先将堆分为大小相等的 Region,避免全区域的垃圾回收。G1的分区示例如下图所示:
G1把堆内存分为大小相等的内存分段,默认情况下会把内存分为2048个内存分段。比如32G堆内存,2048个内存分段每段的大小为16M。这相当于把内存化整为零。内存分段是物理概念,代表实际的物理内存空间。每个内存分段都可以被标记为Eden区,Survivor区,Old区,或者Humongous区。
G1引进了RSet的概念Remembered Set,作用是跟踪指向某个heap区内的对象引用。记录老年代到新生代之间的引用。
如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。
当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
G1垃圾回收过程主要包括三个:
- 年轻代回收(young gc)过程
- 老年代并发标记(concurrent marking)过程
- 混合回收过程(mixed gc)。
1)Young GC
- 阶段1:根扫描 静态和本地对象被扫描
- 阶段2:更新RS 处理dirty card队列更新RS
- 阶段3:处理RS 检测从年轻代指向年老代的对象
- 阶段4:对象拷贝 拷贝存活的对象到survivor/old区域
- 阶段5:处理引用队列 软引用,弱引用,虚引用处理
2)并发标记+垃圾回收
- 初始标记(initial mark,STW) 在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
- 根区域扫描(root region scan) G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
- 并发标记(Concurrent Marking) G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
- 最终标记(Remark,STW) 该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
- 清除垃圾(Cleanup,STW) 在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
了解并发标记的三色标记算法。
收集器总结:
- 4.方法区回收条件:
只有同时满足以下三个条件才会被回收!
1)所有实例被回收
2)加载该类的ClassLoader被回收
3)Class对象无法通过任何途径访问(包括反射)
java内存模型
Java内存模型的主要目标是定义程序中变量的访问规则。即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。需要注意的是这里的变量写Java程序中的变量不是完全等同的。这里的变量是指实例字段,静态字段,构成数组对象的元素,但是不包括局部变量和方法参数(因为这是线程私有的)。这里可以简单的认为主内存是java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。但是在堆中的变量如果在多线程中都使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。
- 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,可以简单认为是堆区(仅仅做解释,实际不是)。不是物理内存,这里指的是虚拟机的主内存,它是虚拟机内存中的一部分。
- 工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。线程的工作内存保存了线程需要的变量在主内存中的副本。线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
java内存中线程的工作内存和主内存的交互是由java虚拟机定义了如下的8种操作来完成的。
- lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
- unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
- read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
- load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
- use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
- assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
- store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
执行这些操作需要遵循文中一开始记录的Happens-before原则。
内存泄漏
JAVA GC回收本质上还是判断一个对象是否被引用的方式。如果,JVM误以为此对象还在引用中,无法回收,造成内存泄漏。
8种常见情况:
1)static字段太多
原因:静态字段拥有和整个应用程序一样的生命周期。想一想单例模式的static,对象如果太大就gg了
解决办法:最大限度的减少静态变量的使用;单例模式时,依赖于延迟加载对象而不是立即加载方式。
2)未关闭资源
原因:每次使用JAVA IO流等创建读取流时,JVM都会为这些资源分配内存。
解决办法:使用finally块关闭资源;关闭资源的代码,不应该有异常;jdk1.7后,可以使用try-with-resource块。
3)hashcode()和不正确的equals()
原因:在HashMap和HashSet这种集合中,equal()和hashCode()来比较对象,如果重写不合理,将会成为潜在的内存泄露问题。
比如:当一个对象被存储进HashSet集合中,不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同,始终让GC以为对象有引用,无法回收。
解决办法:用最佳的方式重写equals()和hashCode。
4) 引用外部类的内部类
原因: 非静态内部类的初始化,需要外部类实例才能使用;默认情况下,每个非静态内部类有对外部类的引用,引用了这个内部类,那么外部类对象超出范围后,它也不会被垃圾收集。
解决办法:如果内部类不需要访问包含的类成员,考虑转换为静态类。
5)finalize()方法造成的内存泄露
原因:重写finalize()方法时,该类的对象不会立即被垃圾收集器收集,如果finalize()方法的代码有问题,那么会潜在的引发OOM;
解决办法:避免重写finalize()。
6)ThreadLocal
使用ThreadLocal时,每个线程只要处于存活状态就可保留对ThreadLocal变量的调用。使用不当,就会引起内存泄露。
一旦线程不在存在,ThreadLocals就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到ThreadLocals来保留线程池中线程的变量副本时,ThreadLocals没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。
解决办法:不在使用ThreadLocal时,调用remove()方法,该方法删除了此变量的当前线程值。不要使用ThreadLocal.set(null),它只是查找与当前线程关联的Map并将键值对设置为当前线程为null。
7)常量字符串
原因:读取一个很大的String对象,并调用了intern(),它将放到字符串池中,只要应用程序运行,该字符串就会保留,这就会占用内存,可能造成OOM。
解决办法:增加PermGen的大小,-XX:MaxPermSize=512m(1.7前);升级Java版本,JDK1.7后字符串池转移到了堆中。
引用类型 ; { 强引用、 软引用、 弱引用 、 虚引用 }
JAVA 4种引用类型。
- 强引用
java默认的声明就是强引用。
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null
只要强引用存在,垃圾回收就不会回收它。除非你手动置为空,或者对象的生存周期结束(局部对象,函数调用完毕)。
2. 软引用
JAVA 1.2之后,用java.lang.ref.SoftReference
表示软引用。
当内存不够用时,才会回收软引用对象。如果回收后内存还不够用,才会OOM(Out Of Memory)。
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
if(sr.get()!=null) {
rev = (byte[]) sr.get(); // 还没有被回收器回收,直接获取
} else {
buff = new new byte[1024 * 1024]; // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference<>(buff); // 重新构建
}
如果设置JVM内存只有2M,下面这个代码每次分配1M的内存,看看什么结果:
public class TestOOM {
private static List<Object> list = new ArrayList<>();
public static void main(String[] args) {
testSoftReference();
}
private static void testSoftReference() {
for (int i = 0; i < 10; i++) {
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
list.add(sr);
}
System.gc(); //主动通知垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((SoftReference) list.get(i)).get();
System.out.println(obj);
}
}
}
调用了很多次,只有一个对象时存在的,其他都是null
被回收了。
3.弱引用
比软引用还弱。无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都要回收。
private static void testWeakReference() {
for (int i = 0; i < 10; i++) {
byte[] buff = new byte[1024 * 1024];
WeakReference<byte[]> sr = new WeakReference<>(buff);
list.add(sr);
}
System.gc(); //主动通知垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((WeakReference) list.get(i)).get();
System.out.println(obj);
}
}
还是限定内存2M,每次byte数组使用1M,让其内存不够用,看结果:
所有对象都回收了。
4. 虚引用
PhantomReference
表示虚引用,它是最弱的引用。对象仅有虚引用,和没有引用一样,随时可能被回收。必须要和ReferenceQueue
引用队列一起使用。
看它的PhantomReference
类的源代码:
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
它的get永远返回null
。无法通过虚引用引用对象。
Object obj = new Object();
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj,refQueue);
这没法引用对象,有他妈的啥用呢?
可以用来跟踪对象呗。它的作用在于跟踪垃圾回收过程,在对象被收集器回收时收到一个系统通知。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。 所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。
与软引用和弱引用不同,显式使用虚引用可以阻止对象被清除,只有在程序中显式或者隐式移除这个虚引用时,这个已经执行过finalize方法的对象才会被清除。想要显式的移除虚引用的话,只需要将其从引用队列中取出然后扔掉(置为null)即可。
有个例子:
public class PhantomReferenceTest {
private static final List<Object> TEST_DATA = new LinkedList<>();
private static final ReferenceQueue<TestClass> QUEUE = new ReferenceQueue<>();
public static void main(String[] args) {
TestClass obj = new TestClass("Test");
PhantomReference<TestClass> phantomReference = new PhantomReference<>(obj, QUEUE);
// 该线程不断读取这个虚引用,并不断往列表里插入数据,以促使系统早点进行GC
new Thread(() -> {
while (true) {
TEST_DATA.add(new byte[1024 * 100]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
System.out.println(phantomReference.get());
}
}).start();
// 这个线程不断读取引用队列,当弱引用指向的对象呗回收时,该引用就会被加入到引用队列中
new Thread(() -> {
while (true) {
Reference<? extends TestClass> poll = QUEUE.poll();
if (poll != null) {
System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
System.out.println("--- 回收对象 ---- " + poll.get());
}
}
}).start();
obj = null;
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
e.printStackTrace();
System.exit(1);
}
}
static class TestClass {
private String name;
public TestClass(String name) {
this.name = name;
}
@Override
public String toString() {
return "TestClass - " + name;
}
}
}
程序输出:
终于知道虚引用有什么用了!!!
Object的方法 : { finalize 、 clone、 getClass 、 equals 、 hashCode }