作用:记住下一条jvm指令的执行地址
特点
Java源代码->二进制字节码(jvm指令)->【解释器解释】机器码->CPU执行
栈:线程运行时需要的内存空间
栈帧:每个方法运行时需要的内存,栈由多个栈帧组成
每个线程只能有一个活动的栈帧,对应着当前正在执行的那个方法
public class test {
public static void main(String[] args) {
m1();
}
public static void m1(){
m2(1,2);
}
public static int m2(int a,int b){
int c = a + b;
return c;
}
}
1、垃圾回收是否涉及栈内存?
不需要!栈帧出栈的时候就已经释放掉了
2、栈内存分配越大越好吗?
不是!栈内存太大反而影响到线程数目,采用系统默认的大小即可
3、方法内的局部变量是否线程安全?
如果方法内部局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
-Xss256k
案例1:CPU占用过多
用top定位哪个进程对CPU的占用高
top
用ps命令进一步定位是哪个线程引起的CPU占用过高(显示的线程是10进制)
ps -H -eo pid,tid,%cpu | grep 进程id
用jstack工具将进程中的所有线程列出来(显示的线程id是16进制,因此查找问题的时候需要先将上一步查到的线程id换算成16进制),可以根据线程id找到有问题的线程,进一步定位到有问题的代码行数
jstack 进程id
案例2:程序运行很长时间没有结果
jstack 进程id
Java虚拟机调用本地方法(Native Method)时提供的内存空间
Heap堆:通过new关键字创建的对象都会使用堆内存
特点
1、jps工具
查看当前系统中有哪些Java进程
2、jmap工具
查看堆内存占用情况(只能查看某一时刻)
jmap -heap 进程id
3、jconsole工具
图形界面的,多功能的检测工具,可以连续监测
案例
垃圾回收之后,内存占用依然很高
解决方法:使用jvisualvm可视化工具dump下堆内存快照进行分析
存储了跟类的结构相关的一些信息,包括类的成员变量、方法数据、成员方法和构造器方法的代码等。
1、方法区在虚拟机启动时被创建,逻辑上是堆的组成部分
2、方法区是规范,永久代(jdk1.6,占用的是堆内存)和元空间(jdk1.8,占用的是系统的内存)只是实现。
3、注意看元空间里面的包含:类、类加载器、常量池
4、StringTable1.6是放在方法区,1.8则放到了堆空间
jdk1.8以前会导致永久代内存溢出:java.lang.OutOfMemoryEeror:PermGen space
需要先设置元空间内存大小,方便演示:-XX:MaxMetaspaceSize=8m
jdk1.8之后会导致元空间内存溢出:java.lang.OutOfMemoryEeror:Metaspace
一文搞懂各种常量池
运行时常量池:常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实的地址。
public class Test2 {
//StringTable是一个hashtable结构,不能扩容
public static void main(String[] args) throws InterruptedException {
// 常量池中的信息,都会被加载到运行时常量池中,这时a b ab都是常量池中的符号,还没有变为Java字符串对象
String s1 = "a";// 只有执行到这一段代码时,a才会在StringTable中创建
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;// new StringBuilder().append(s1).append(s2).toString(); StringBuilder().toString()-->new String("ab");
String s5 = "a" + "b";// 创建一个字符串String ab,先去常量池中查找,已经有了,就不创建了。底层为编译期的优化,已经有一个了就不另外创建了
// s3是在常量池中;s4是一个对象,存放在堆中 答案就是false
System.out.println(s3 == s4);
// 他俩其实就是同一个
System.out.println(s3 == s5);
// 总结:所有的字符串的对象都是懒惰的,只有在用到时才会加载到StringTable中,加载的时候会先查找,没有才会放进去,有了就直接使用
}
}
1、常量池中的字符串仅仅是符号,第一次用到时才变为对象
2、利用串池的机制,来避免重复创建字符串
3、字符串变量的拼接原理时StringBuilder
4、字符串常量拼接的原理是编译期优化
5、可以使用intern方法,主动将串池中还没有的字符串对象放入串池
public class Test3 {
public static void main(String[] args) {
// 此时的"a","b"都在常量池中:["a","b"]
String s = new String("a") + new String("b");// 相当于new String("ab"),存放于堆中
// 将这个字符创对象尝试放入串池,如果有则不放入,如果没有则放入串池,会把串池中的对象返回
String s2 = s.intern();
System.out.println(s2 == "ab");
}
}
jdk1.6在永久代中,不易被回收
jdk1.8之后在堆中,majorGC就可以回收掉,节约空间
StringTable中存在垃圾回收
StringTable底层是一个HashTable,因此调优就是调整HashTable桶的个数:
-XX:StringTableSize=10000
总结:当项目中字符串很多时,可以考虑调整StringTable,增加桶的个数,减少Hash碰撞
直接内存
1、常见于NIO操作时,用于数据缓冲区
2、分配成本较高,但读写性能高
3、不受JVM内存回收管理
操作系统专门划出来一块内存,供Java直接使用,当然,操作系统也可以使用。
public class Demo4 {
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
ByteBuffer allocate = ByteBuffer.allocate(_100Mb);
}
}
引用计数法的弊端:循环引用
首先确定一系列根对象,何为根对象?即不可能被回收的对象
强引用
描述:直接和GC Root相连的对象,如上图的A1对象,
回收时机:即使垃圾回收完内存不够也不会被回收掉
软引用
描述:不直接和GC Root相连
回收时机:垃圾回收结束内存还是不够时,会回收掉软引用
应用:当读取一些大文件时,可以将读取的文件设为软引用,重读读取时可以避免内存溢出
public class Test4 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
soft();
}
public static void soft(){
ArrayList<SoftReference<byte[]>> softReferences = new ArrayList<>();
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联引用队列,当软引用引用的byte数组被回收时,软应用自己就会加入引用队列中去
SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4MB],queue);
System.out.println(softReference.get());
softReferences.add(softReference);
System.out.println(softReferences.size());
}
System.out.println("循环结束");
// 清除无用的软引用本身
Reference<? extends byte[]> poll = queue.poll();
if (poll != null){
softReferences.remove(poll);
poll = queue.poll();
}
System.out.println("==============================");
for (SoftReference<byte[]> softReference : softReferences) {
System.out.println(softReference.get());
}
}
}
```
优点:速度快
缺点:容易产生内存碎片,导致内存不连续
标记过程和“标记-清除算法”一样,但后续步骤是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
优点:不会产生内存碎片
缺点:由于整理要移动对象,导致效率较低
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次性清理掉。
优点:不会产生内存碎片
缺点:要占用双倍的内存空间
新创建的对象,默认会存放在伊甸园区,当伊甸园区满了之后,就会触发一次Minor GC,Minor GC采用复制算法,将存活的对象放入幸存区TO中,同时给存活的对象的寿命+1(此时伊甸园区就回收掉释放出空间),Minor GC完成之后,会交换幸存区FROM和幸存区TO。
当伊甸园区再次满了之后,开始第二次垃圾回收,同样会将幸存的对象放入幸存区FROM,同时会对幸存区TO中进行一次垃圾回收,没有被回收掉的对象同样会被放进幸存区FROM,而且寿命+1,最后,交换幸存区FROM和幸存区TO
当幸存区FROM中的对象的寿命达到15时,会被放倒老年代中。老年代满了之后,会触发一次Minor GC,如果回收之后空间仍然不足,那么触发Full GC
重要:Minor GC算法:复制,Full GC算法:标记清除/标记清理
JDK8默认的垃圾收集器为: Parallel Scavenge(新生代,标记复制算法)+ Serial Old(老年代,标记整理算法)
特点:单线程、堆内存较小,CPU核心数比较少
开启串行垃圾回收器:
-XX:+UseSerialGC = Serial + SerialOld
说明:Serial工作在新生代,采用复制算法,SerialOld工作在老年代,采用标记整理算法
相关的垃圾收集器:
1、ParNew,实质上是Serial收集器的多线程并行版本,常用搭配:ParNew(新生代)+CMS(老年代)
2、Serial Old:Serial收集器的老年代版本,采用标记整理算法,常用搭配:Parallel Scavenge(新生代)+ Serial Old(老年代)
特点:多线程、堆内存较大,多核CPU、让单位时间内,STW的时间最短,多个垃圾回收线程同时运行,但用户线程会暂停
开启并行的垃圾回收器:
-XX:+UseParallelGC(新生代、复制算法) ~ -XX:+UseParallelOldGC(老年代、标记整理算法)
相关的垃圾收集器:
1、Parallel Scavenge:新生代收集器,基于标记复制算法,常用搭配:Parallel Scavenge(新生代)+ Serial Old(老年代)
2、Parallel Old:是Parallel Scavenge的老年代版本,基于标记整理算法
二者搭配就是JDK8默认的垃圾收集器
特点:多线程、堆内存较大,多核CPU、垃圾回收时尽可能让单次STW的时间最短、工作的同时用户线程也能并发运行
开启:
-XX:+UseConcMarkSweepGc(老年代) ~ -XX:+UseParNewGC(新生代) ~ SerialOld(并发失败时的补救措施)
-XX:+CMSScavengeBeforeRemark:由于重新标记比较耗时,因此在重新标记前就清理一下新生代,降低标记时长
常用搭配:ParNew(新生代)+CMS(老年代)
面试必答:
1、基于标记清除算法实现
2、工作流程是:初始标记、并发标记、重新标记、并发清除
3、缺点:
1、会产生内存碎片(因为是基于标记清除算法,解决:增大Full GC的频率)
2、重新标记阶段比较耗时(解决:在执行重新标记之前先做一次Young GC)
3、Promotion Failed问题(导致的原因是在进行Minor GC的时候,老年代空间不足,无法放下大对象,解决:对内存占用率达到60%的时候就开始GC)
重新理解CMS
特点:
1、同时注重吞吐量和低延迟,默认暂停时间是200ms
2、超大堆内存,会将堆划分为多个大小相等的Region
3、整体上是标记+整理算法,两个区域之间是复制算法
相关参数:
-XX:UseG1GC
-XX:G1HeapRegionSize=size
-XXMaxGCPauseMillis=time
面试必答:
1、特点:同时追求高吞吐量和低延时、能够预测GC停顿时间。
2、内存布局:把堆内存划分为多个大小相等的独立区域即Region。每一个Region都可以根据需要去扮演Eden区,Survivor区,Old区,Humongous区。(注:Humongous区是一类特殊区域专门存放大对象)。
3、回收过程:在回收时以Region作为最小的回收单位,G1会预测出每个Region的回收时间,回收后得到的内存大小以此计算出的该Region回收的"价值",根据用户设置的期望GC停顿时间,每次回收优先处理回收价值最大的Region,这也是G1(Garabage First)名字的由来原因。
4、好处:一次回收不用针对全部内存,只需要先回收垃圾最多的region,提高了垃圾收集的效率、变相实现了只有新生代才有的复制算法,极大减少了空间碎片的产生(后续会提到)
5、缺点:跨region引用(解决:引入了RSet(Remembered Set),用于记录其他Region指向该Region的引用)
重新理解G1
三色标记详解
1、字符串去重
创建两个相同的字符串:
String s1 = new String("abc");// char[]{'a','b','c'}
String s2 = new String("abc");// char[]{'a','b','c'}
将所有新分配的字符串放入一个队列,当新生代回收时,G1并发检查时否有字符串的重复,如果他们值一样,让他们引用同一个char[]
2、并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用时,则卸载它所加载的所有类
3、回收巨型对象
一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,回收时被有限考虑。
4、并发标记起始时间
类加载的阶段:加载-链接-初始化
链接阶段又分为三个小的阶段
初始化即调用类的构造方法,虚拟机保证这个类的构造方法的线程安全
概括得说,类初始化是【懒惰的】,以下几种情况会导致类的初始化:
不会导致类初始化的情况:
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去完成加载。