



作用: 记住下一条JVM指令的执行地址(记录当前的代码执行到了第几行)
实现方法: 通过寄存器实现
特点:
栈帧: 每个方法运行时需要的内存。
虚拟机栈: 每个线程运行时所需要的内存.
一个栈内有多个栈帧组成,栈桢对应每次方法调用所占用的内存,每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
问题辨析:
栈内存溢出:
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法: 一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java方法:该方法的实现由非java语言实现。
本地方法栈:用于管理本地方法的调用
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
通过new关键字,创建对象都会使用堆内存。
特点:
堆内存溢出:大量对象占据了堆空间,这些对象都持有强引用导致无法回收,当对象大小之和大于Xmx参数指定的堆空间时就会发生堆溢出。
解决方法:
方法区:保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中;
方法区的基本特点:
方法区内存溢出
方法区与永久代、元空间之间的关系
字符串常量池(Stringtable):
字符串变量拼接
public class Demo1_22 {
//常量池中的信息都会被加载到运行时常量池中,这时a b abc都是常量池中的符号,还灭有变为java字符串对象
// ldc #2会把a符号变为“a“字符串对象,在串池中找”a”,如果没有,放入串池
// ldc #3会把a符号变为“b“字符串对象,在串池中找”b”,如果没有,放入串池
// ldc #4会把a符号变为“ab“字符串对象,在串池中找”ab”,如果没有,放入串池
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //new StringBuilder().append("a").append("b") 创建新的值为ab的对象,存到s4
String s5 = "a" + "b"; // javac在编译期的优化,结果已经在编译器确定为ab
System.out.println(s3 == s4); //false s3在串池中 s4相当于new出来的对象,保存在堆中
System.out.println(s3 == s5); //true javac在编译期的优化,结果已经在编译器确定为ab
}
}
字符串延迟加载
public class Demo1_22_StringNums {
public static void main(String[] args) {
int x = args.length; //x: 0 args: []
System.out.println(); //字符串个数3204
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5"); //字符串个数3209
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5"); //字符串个数3209
}
}
intern方法:
String s = new String("a") + new String("b");
//先在常量池放入["a", "b"]
// 在堆中存入new String("a") new String("b")
// StringBuilder拼接了新的字符串 new String("ab")存在堆中
String s1 = s.intern();//将字符串中的对象尝试放入串池,如果有则不会放入,没有则放入串池,会把串池中的对象返回
System.out.println(s1 == "ab"); //true 因为调用intern()已经将"ab"放入串池。
System.out.println(s == "ab"); //true 因为调用intern()已经将"ab"放入串池。
//此时串池["a", "b", "ab"]
String s = new String("a") + new String("b");
//先在常量池放入["a", "b"]
// 在堆中存入new String("a") new String("b")
// StringBuilder拼接了新的字符串 new String("ab")存在堆中
String x = "ab";
String s1 = s.intern();//将字符串中的对象尝试放入串池,如果有则不会放入,没有则放入串池,会把串池中的对象返回
System.out.println(s1 == "ab"); //true
System.out.println(s == "ab"); //false 执行String x = "ab"时已经将ab放入串池,intern()便不会放入
//此时串池["a", "b", "ab"]
StringTable面试题
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; //ab放入常量池
String s4 = s1 + s2; //运行期间通过StringBu ilder进行拼接,存在堆中,相当于new String("ab")
String s5 = "ab";
String s6 = s4.intern();
System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //true
System.out.println(s3 == s6); //true
String x2 = new String("c") + new String("d"); //new String("cd")
String x1 = "cd";
x2.intern();
System.out.println(s1 == s2); //false 如果x2.intern()先提一行 则为true
StringTable存储位置:
StringTable性能调优:
定义:
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式 (补充内容,需要掌握):
指针碰撞 :
适用场合 :堆内存规整(即没有内存碎片)的情况下。
原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
使用该分配方式的 GC 收集器:Serial, ParNew
空闲列表 :
适用场合 : 堆内存不规整的情况下。
原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
使用该分配方式的 GC 收集器:CMS
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
可能因为循环引用导致内存泄露
哪些对象作为GC Root?
查看根对象方法: Eclipse Memory Analyzer
软引用、弱引用可以配合引用队列进行工作,被回收的时候进入引用队列,使用引用队列进行其他操作。
虚引用、终结器引用必须关联引用队列。
我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。
只有所有GC Roots对象都不通过强引用引用该对象,该对象才能被垃圾回收。(没有直接引用才回收)
没有被直接的强引用引用时,垃圾回收时有可能会被回收(内存不足时会被回收)。
没有被直接的强引用引用时,只要发生垃圾回收就会回收
是四种引用类型中最弱的一个。一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。主要配合ByteBuffer引用
虚引用引用的对象被垃圾回收时,会放入引用队列,从而间接地使用一个线程调用Unsafe.freeMemory()方法释放直接内存。
对象重写了finalize()终结方法,并且没有强引用引用时,可以被当成垃圾回收,使用终结器引用回收 。
被垃圾回收时,把终结器引用加入引用队列,finalizeHandeler()线程在引用队列进行回收。
finalizeHandeler()优先级低,迟迟不能回收
分为两个阶段,先寻找堆中没有被GC Root直接或间接引用的对象进行标记,再进行清除

优点:速度快
缺点:有可能产生过多内存碎片,导致存储效率不高。

优点: 内存更紧凑,连续的空间更多,不会造成标记清除的碎片问题。
缺点: 速度慢
把内存区域划分成大小相等的两部分区域,如图,左边称为FROM右边称为TO,右边区域空闲

把未被引用的对象标记为垃圾

再把FROM区存活的对象复制到TO区,复制的过程中会进行碎片整理,此时FROM区全是垃圾,可以全部进行清除

清空后交换FROM和TO的位置

优点:不会产生碎片
缺点:需要双倍的内存空间
堆内存分成两块:新生代、老年代

长时间使用的对象放在老年代,用完可以丢弃的放在新生代。
新创建的对象首先放在Eden区,当Eden区满了,触发一次垃圾回收,即Minor GC,把存活的对象存在幸存区To中,让幸存对象寿命+1。

紧接着交换From和To的位置。即为第一次垃圾回收产生的效果。

后续的垃圾回收,不仅要检测Eden区,还要检测幸存区中的对象是否存活,重复上述操作。
minor GC会出发stop the world即进行垃圾回收的时候,暂停其他用户线程,等垃圾回结束, 用户线程才恢复运行

当幸存区的对象超过15次(最大),将其晋升到老年代中

当新生代老年代都几乎全满,会触发一次Full GC,触发整个新生代和老年代的垃圾回收
Full GC采用标记清除或标记整理,回收效率回收速度都较低,STW的时间更长。
-XX:+UseSerialGC = Serial + SerialOld

-XX:+USeParallelGC ~ -XX:+USeParallelOldGC
-XX:+UseAdaptiveSizePolicy //采用自适应新生代的大小调整策略
-XX:GCTimeRatio=ratio //目标1: 根据设置的目标调整垃圾回收时间和总时间的占比
-XX:MAxGCPauseMillis //目标2: 最大暂停毫秒数
//上述两个目标是冲突的
-XX:ParallwlGCThreads=n //控制GC运行数量

⼀种以获取最短回收停顿时间为⽬标的收集器
采用标记清除算法实现
整个过程分为四个步骤

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

取代了CMS垃圾回收器
特点
使用场景:

相关JVM参数
-XX:+UseG1GC
-XX:+G1HeapRegionSize=size
-XX:MAxGCPauseMillis=time
JDK13最新的垃圾回收器。stw的情况会更少
JDK 13 的最新垃圾回收器ZGC


CMS和G1,老年代内存不足时触发的垃圾回收,需要分两种情况,以G1为例:


