java虚拟机
1.JVM的运行时数据区
根据JVM规范,JVM内存共分为虚拟机栈,堆,方法区,程序计数器,本地方发栈五个部分.其中堆与方法区是线程共享,其他三个是线程私有的
a.java虚拟机栈
线程私有;每个方法在执行的时候会创建一个栈帧,存储了局部变量表,操作数栈(如sum = i + j 处理后将结果存储到局部变量表中),动态连接,方法返回地址等;每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
如写一个栈溢出(其实就是方法的递归调用,且没有出口)
int num = 1; public void testStack() { num++; this.testStack(); }
b.堆
public void testHeap() {
for(;;) {
ArrayList<String> list = new ArrayList<>(1000);
}
}
c.方法区
线程共享:被所有线程共享的一块区域;用于存放类信息,常量,静态变量,即使编译器编译后的代码.
存放的信息包括:
类的基本信息:
1)每个类的全限定名
2)每个类的直接超类的全限顶名
3)该类是类还是接口
4)该类的访问修饰符
5)直接超类接口的全限定名的有序列表
已装载类的详细信息
1)运行时常量池:方法区中,每个类都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了.诸如文字字符串,finale变量值,类名和方法名常量,他们以数组的形式通过索引被访问,是外部调用与类联系及类型对象化的桥梁
2)字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。字段名称指的是类或接口的实例变量或类变量, 字段的描述符是一个指示字段的类型的字符串,如private A a=null;则a为字段名,A为描述符,private为修饰符。
3) 方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区。)
4) 静态变量:就是类变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块。
5)到类classloader的引用:到该类的类装载器的引用。
6)到类class 的引用:jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。
d.程序计数器
线程私有;是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
e.本地方发栈
线程私有;主要为虚拟机使用到的Native方法服务。
2.JVM内存模型(主要针对的是java的堆和方法区)
整个堆大小=年轻代大小+年老代大小+持久代大小(方法区,默认情况下256M)
3.java垃圾回收机制
java中,GC的对象主要是堆空间和永久区
a.如何判断某个对象是否为垃圾?
引用计数法和可达性分析法
1)引用计数法
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
引用计数法的问题
引用和去引用伴随加减法,影响性能
很难处理循环引用的问题
2)可达性分析法
从GCRoots节点一直往下走,如果走不通,说明走不通的那些对象是不可用的,那就是垃圾.
可以作为GCRoots的对象
虚拟机栈(局部变量表)
方法区的类属性所引用的对象
方法区中常量所引用的对象
本地方法栈中引用的对象
b.如何回收(回收策略,垃圾回收算法)
1)标记-清除算法
2)标记-整理算法(也就是标记-压缩算法)
3)复制算法
4)分代收集算法
1.标记-清除(其他算法的基础)
标记-清除算法是现代收集算法的思想基础.标记-清除算法将垃圾回收分为两个阶段,标记和清除阶段.一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始可达的对象那个因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
2.标记-整理(标记-压缩算法)(主要针对老年代的收集算法,因为老年代相对来说存活的对象比较多)
标记-整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
3.复制算法(主要针对新生代的收集算法,因为新生代存活的对象比较少)
与标记-清除算法相比,复制算法是一种相对来说比较高效的回收算法,但是此种算法并不适用于存活对象比较多的场合如老年代,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
4.分代收集算法
标记-整理和复制算法结合使用,针对新生代采用复制算法,针对老年代采用标记整理算法。
c.垃圾回收器
1)Serial(串行回收)
-XX:+UseSerialGC来开启
最基本,发展最悠久的收集器
单线程垃圾收集器(先是程序跑一段时间,然后所有线程又停下来,垃圾收集器开始收集垃圾)(打j扫垃圾的时候就不能扔垃圾啦)
桌面应用使用比较多
优点:简单.对于单CPU的情况由于没有多线程交互的开销,反而可以更加高效,是Client模式下默认的新生代收集器
缺点:Stop-The-World
2)Parnew(并发回收)(Serial的多线程加强版)
-XX:+UseParNewGC开启
3)Parallel Scavenge(并行回收)(目标:达到一个可控制的吞吐量)
-XX:+UseParallelGC开启
复制算法(新生代收集器)
多线程收集器
达到一个可控制的吞吐量(cpu用于运行用户代码的时间与cpu消耗的总时间的比值)
-XX:MaxGCPauseMills 垃圾收集器最大停顿时间(如果设置的最大停顿时间短了,则回收的频率就增大了咯)
-XX:GCTimeRatio 吞吐量大小
停顿时间越短就越适合与用户交互的程序,良好的相应速度能提升用户体验
而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务.
4)Cms(Concurrent Mark Sweep并发标记清除)(大多数互联网公司现在都在是哦那个这个收集器)
标记清除算法(老年代收集器)
工作过程
-
- 初始标记:值标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 重新标记:修正并发标记期间因用户程序继续运行而导致标记发生改变的那一部分对象的标记
- 并发清理:并发收集垃圾
优点
并发收集(边扔边打扫)
低停顿
缺点
占用大量CPU(当CPU数量在4个以上时,并行回收时垃圾收集线程不少于25%的CPU资源,而在不足4个,可能影响更大)
无法处理浮动垃圾(在并发清理阶段用户线程还在运行着,自然会有新的垃圾不断产生,这一部分只能留待下一次GC再清理)
空间碎片(由于使用的是标记-清除算法
出现Concurrent Mode Failure
-XX:ParallelCMSThreads:手工设置CMS线程个数,CMS默认启动的线程数是(ParalleleGCThreads+3/4)
-XX:+UseConcmarkSweepGC:开启
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后发出垃圾收集,默认值为68%
-XX:+UseCMSCompactAtFullCollection:由于CMS收集器会产生碎片,此参数设置在垃圾收集后是否需要一次内存碎片整理工程
-XX:+CMSFullGCBeforeCompaction:设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通过UseCMSCompactAtFullCollection一起使用
5)G1(Garbage First)(JDK1.9里)
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
d.GC何时开始?
堆:
新生代 8:1:1,空间利用率百分之九十,因为假设Eden区,假设new了8M对象,再new1M对象,则这1M放在s0区域,Eden区域满了,执行Minor GC,存活率只有百分之十,那么这个百分之十放在s1中,将另外的全部清除掉。\
Eden 伊甸园
Survivor 存活区s0
Tenured Gen,s1
新生代:划分为三个区域:原始区(Eden)和两个小的存活区(Survivor),两个存活区按功能分为From和To。绝大多数的对象都在原始区分配,超过一个垃圾回收操作仍然存活的对象放到存活区。垃圾回收绝大部分发生在年轻代。
老年代:存储年轻代中经过多个回收周期仍然存活的对象,对于一些大的内存分配,也可能直接分配到永久代。
持久代:存储类、方法以及它们的描述信息,这里基本不产生垃圾回收。
e.内存分配策略
优先分配到Eden(当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC)
大对象直接分配到老年代(如byte数组等)
长期存活的对象直接进入老年代
为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。每熬过一次年龄就增加1.当达到(默认是15,有参数可以设置)一定程度,就会被晋升到老年代中
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保(新生代内存不够向老年代中借)
4.java类加载
a.运行流程
b.基本结构
类加载器,执行引擎,运行时数据区,本地接口
Class Files -> ClassLoader -> 运行时数据区 ->执行引擎,本地库接口 ->本地方法库
类的装载
加载:查找并加载类的二进制数据
通过一个类的全限定名来获取定义此类的二进制字节流(虚拟机并没有指明二进制字节流要从一个Class文件中获取,准确地说根本没有指明要从哪里获取、怎样获取等)
从ZIP包中读取,这很常见,最终成为JAR、EAR、WAR格式的基础
从网络中获取,典型应用Applet
运行时计算生成,这种场景使用最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“$Proxy”的代理类的二进制字节流
......
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象(并没有明确规定是在java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面)这个对象那个将作为程序访问方法区中的这些类型数据的外部接口。
public class ClassLoaderDemo3 { public static void main(String[] args) { SingleTon singleTon = SingleTon.getinstance();//主动使用,SingleTon类会被加载和初始化 System.out.println("count1 = " + SingleTon.count1);//结果为1 System.out.println("count2 = " + SingleTon.count2);//结果为0(因为在初始化时,先是new SingleTon()里面执行++但此时count1与count2都是默认值0,执行完后count2又被赋值为0) } } class SingleTon{ private static SingleTon singleTon = new SingleTon(); public static int count1; public static int count2 = 0; private SingleTon() { count1 ++; count2 ++; } public static SingleTon getinstance() { return singleTon; } }
c.类加载器使用双亲委派模型
1)JDK已有的类加载器
2)双亲委派模型
3)为什么要使用这种双亲委派模型呢?