一、JVM的内存划分:
https://www.cnblogs.com/lifescolor/p/5481588.html
线程共享区域:
1、java堆:是jvm内存管理中最大的一块,存放new出来的对象实例。
2、方法区:主要存放的是已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
线程私有区:
3、虚拟机栈:虚拟机栈生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用到执行完毕的过程中,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
补充:局部变量表中存放了编译期可知的各种基本数据类型、对象的引用类型。局部变量表中需要的内存空间在编译期间完成分
配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间完全是确定的,方法运行期间不会改变局部变量表的大小。
4、本地方法栈:与虚拟机栈很相似,区别在于虚拟机栈是为执行Java方法服务的,而本地方法栈是为本地方法服务的。
5、程序计数器:一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
补充:如果线程正在执行的是一个java方法,那计数器记录的是正在执行的jvm字节码的指令的地址;若是本地方法,则这个计数器为空。
二、JVM的垃圾回收:
https://blog.youkuaiyun.com/SEU_Calvin/article/details/51892567
https://www.cnblogs.com/1024Community/p/honery.html
2.1、哪些内存需要回收(什么是垃圾):
进行垃圾回收前首先要判断哪些对象是垃圾(可回收的)。介绍两种判断对象是否存活的算法。
2.1.1、引用计数算法:
给每一个对象分配一个引用计数器,每当有地方引用该对象时,该对象的引用计数器+1(a = b,则b对象的计数器+1);当引用失效时(引用超过了生命周期或被设置为一个新值),引用计数器的值-1。当该对象的引用计数器的值为0时,说明该对象可回收。
但是该算法有一个缺点,无法检测出循环引用。
public class ReferenceFindTest {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
如上,循环引用,即使object1和object2都为null,都不会被访问,但是垃圾回收器还是不能回收它们。
2.1.2可达性分析算法:
从GC ROOT节点开始,向下搜索,搜索所走过的路径为引用链,当一个节点到GC ROOT没有任何引用链时,证明该对象不可用(垃圾,可回收)。
在 JAVA中,可以作为GC ROOT的对象有:
(1)虚拟机栈中引用的对象
(2)方法区中类静态属性和常量引用的对象
(3)本地方法栈中Native方法引用的对象
2.2 Java中的四中引用:
引用强度逐渐减弱。
-
强引用
强引用不会被GC回收,如Object obj = new Object(); -
软引用
描述一些还有用但非必须的对象,如果内存空间不足了,就会回收这些对象。软引用可用来实现内存敏感的高速缓存。 -
弱引用
与软引用的区别在于:一旦发现弱引用的对象,无论内存空间是否足够,都会回收它。弱引用的对象能生存到下一次垃圾回收之前,一但被发现就会被回收。 -
虚引用
虚引用必须和引用队列(ReferenceQueue)联合使用。
2.3 对象死亡之前的最后一次挣扎:
finalize()方法:
垃圾回收器将对象从内存中清除出去之前做必要的清理工作。
就像一个对象的遗书,把一个对象杀了之前让对象最后做点什么,甚至它能再复活自己。
如下面的代码。先赋值为null, 这个时候GC已经注意到这个s变量了,然后你又手动通知GC过来处理,GC正要收回内存时,发现该对象覆盖了Object的finalize方法,于是GC叹了口气,给了这个对象最后一个活动的机会,然后这个对象竟然有给自己新赋值,拯救了自己。
public class Sub2 {
static Sub2 s ;
public static void main(String[] args) throws InterruptedException{
s = new Sub2();
s = null;
System.gc();
Thread.sleep(1000);
s.print();
}
public void finalize() {
System.out.println("我要被回收了...");
s = new Sub2();
}
public void print() {
System.out.println("我还活着,我还能说话。。。");
}
}
三、常用的垃圾回收算法
1、复制算法(Copying算法)
复制算法将可用内存按照容量划分为大小相等的两块,每次只使用一块。当其中一块内存用完了,就将这块内存中还存活的对象复制到另一块内存上,然后再将第一块内存上的空间全部清理掉。
复制算法这样不容易产生内存碎片,且运行高效。
但该算法导致可用内存缩减为原来的一半,若存活对象很多,那么复制算法的效率会大大降低。
2、标记-清除算法(Mark-Sweep算法)
分为两个阶段:标记阶段和清除阶段
标记阶段:标记出所有需要回收的对象;
清除阶段:回收被标记的对象占用的空间。
一个严重的问题是:容易产生内存碎片,碎片太多会导致为大对象分配空间时因为空间不足而提前出发GC
3、标记-整理算法(Mark-Compact算法)
首先标记出所有需要回收的对象,完成标记后,将存活的对象移向一端,然后清理掉端边界以外的所有内存(只留下了存活着的对象)。
解决了内存碎片的问题。
4、分代收集算法(Generational Collection)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
核心思想:将堆区分为老年代和新生代(根据对象存活的生命周期来划分的);
老年代每次垃圾收集时只有少量的对象需要被回收;
新生代每次垃圾收集时都有大量的对象需要被回收。
大部分垃圾收集器对新生代都采取复制算法,因为新生代每次都要回收大部分对象,复制操作的次数少,所以使用复制算法效率较高。
一般将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(比例8 : 1 : 1),每次使用Eden空间和一块Survivor空间,当进行回收时,将还存活的对象复制到另一块Survivor区中,然后清理掉Eden区和SurvivorA区的空间。进行了第一次GC后,使用的就是Eden区和SurvivorB区了,如此反复循环。
补充:
当SurvivorB区的空间不足以存放Eden区和SurvivorA区的存活对象时,就将存活对象直接存放至老年代。若是老年代也满
了,会触发一次Full GC,也就是新生代和老年代都进行回收。(不知是否是正确的知识点)
当对象在新生代躲过一次GC的话,对象年龄加1,默认情况下,对象年龄达到15时,会被移动到老年代中。一般来说,大对象(指需要大量连续存储空间的对象)直接会被分配到老年代中。
在堆区之外的方法区中还有一个持久代,用来存储class文件,静态对象,方法描述等。对持久代的回收主要回收两部分内容:废弃常量和无用的类。(JDK1.8之后持久代被完全移除,换为:Metaspace 元空间)
补充:
新生代发生的GC也叫Minor GC,发生频率较高。
老年代内存满时会触发Full GC,发生频率较低。
什么时候会触发GC:
(1)Minor GC:当Eden区满的时候,会触发Minor GC
(2)Full GC:
|- 调用System.gc()时
|- 老年代空间不足时
|- 持久代空间不足时
介绍一些有关堆的JVM常见的配置方式:
-Xss:栈内存的大小
-Xms:初始堆的大小
-Xmx:最大堆的大小
-XX:NewSize=n:设置新生代的大小。
-XX:NewRatio=n:设置老年代和新生代的比例,比如-XX:NewRatio=3,则老年代:新生代=3:1
-XX:SurvivorRatio=n:设置Eden和两个Survivor的比列,比如-XX:SurvivorRatio=3,则Eden:Survivor:Survivor=3:1:1
-XX:MaxPermSize=n:设置持久代的大小
四、典型的垃圾回收器
1、CMS:
一种以获取最短回收停顿时间为目标的收集器,是一种并发收集器,采用标记-整理算法。
2、G1:
面向服务端应用的收集器,能充分利用多CPU,多核环境;是一块款并行与并发收集器,并且能建立可预测的停顿时间模型
五、java的类加载过程
- 加载: 简单来说,加载就是将class字节码文件从各个来源通过类加载器装载入内存中。
- 链接: 分为3部分
1、验证: 保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
2、准备: 为类变量分配内存,并且赋予初值。
3、解析: 将常量池中的符号引用替换为直接引用。 - 初始化: 对类变量初始化,是执行类构造器的过程。换句话说,只对static变量进行初始化。