1.字符串常量存储在方法区中,字符串字面常量进行+操作,(eg:String a = "land" + "over";)不会使用StringBuffer.append()方法,字符串常量之间+操作在编译期间会被优化,进行常量叠加,字符串对象(eg:String a = "land" ,String b = “over" ,String c = a+ b; ) 之间相加则会使用StringBuffer.append()方法,注意,字符串常量和字符串不是一个,字符串是一个对象,被存储在堆中。
2.常量叠加:在编译器对Java代码进行优化,即将常量表达式进行计算,在运行期间就不用计算。并不是所有的常量都进行叠加,必须编译时能确定其值的常量,因此满足一下要求才会进行常量叠加:
1. 字面量是编译期常量(数字字面量,字符串字面量等)。
2. 编译期常量进行简单运算的结果也是编译期常量,如1+2,”a”+”b”。
3. 被编译器常量赋值的 final 的基本类型和字符串变量也是编译期常量。
3.语法糖:糖衣语法,即使用某种语法,使的开发更加方便,Java中的语法糖有:泛型与类型擦除、自动装箱和拆箱、遍历循环、变长参数、条件编译、内部类、枚举类、断言语句、对枚举和字符串的switch支持、在try语句中定义和关闭资源。
4.Java源码编译过程:
词法和语法分析:将将代码字符串转为token序列,然后根据语法由token序列生成抽象语法树,然后将符号输入到符号表中,
注解处理:用于处理用户自定义的注解,注解是为了节省代码的编写
语义分析和生成class文件:基于抽象语法树进行语义分析,将语法树中的名字,表达式等元素与变量,方法,类型联系;检查变量使用前是否声明,确定泛型参数,类型匹配,异常是否被捕获,进行常量折叠,接触语法糖,将泛型Java转换为普通Java,然后生成class文件。
5.类加载机制速记:LLI,即装载(load) ,链接(link),初始化(Initiallize),链接又细分为效验,准备,解析。
装载和链接完成后,会将二进制字节码转化为Class对象,初始化过程不是类加载必须触发的,但最迟必须在初次主动使用对象前执行。
准备:就是将字节码加载到JVM中,通过类的全限定名和类加载器来加载;
链接:对二进制字节码的格式进行效验,初始化装载类的静态变量以及解析类中调用的接口,类;
初始化:执行类中静态初始化代码,构造器代码,静态属性的初始化。
类加载器使用双亲委派机制进行类的加载,bootstrap c. Extension c . System c。
6.字节码执行:Java字节码的执行是基于栈的体系结构,线程在被创建后,会有PC和栈帧,在方法被调用的时候,就会创建栈帧,栈帧包括局部变量区和操作数栈两部分,局部变量区用于存放方法中的局部变量和参数,操作数栈用于存放方法执行过程中产生的中间结果。
注意:Java字节码是一种中间代码,JVM在运行是对其进行解释和执行,JVM有自己的一套指令,进行执行。
SDK在栈上的优化有:栈顶缓存和部分栈帧共享
栈顶缓存:即对于频繁的将值放入操作数栈,导致寄存器和内存不断的进行交换,栈顶缓存即将操作数栈顶直接缓存在寄存器上,即直接在寄存器上计算,然后放回操作数栈中。
栈帧共享:当方法内部调用另一个方法的时候,通常传入另一个方法的参数为已存放在操作数栈的数据,JDK进行优化,即后一个方法可将前一方法的操作数栈作为当前方法的局部变量,从而节省数据copy带来的消耗。
7.JIT编译器,即JDK提供将字节码编译为机器码的支持,编译在运行时进行,通常称为JIT编译器。JDK在执行过程中对执行频率高的代码进行编译,对执行不频繁的代码继续采用解释的方式。
JDK有两种,一种是client,一种是server,client称C1,少量的优化,适合桌面交互式应用,采用的优化有:方法内联,去虚拟化,冗余消除等。
方法内联:即当在方法中进行方法的调用时,会涉及多个参数的传递,返回值等,因此方法内联,即把调用到的方法的指令直接植入到当前方法中。
去虚拟化:即在装载class文件后,进行类层次的分析,当发现类中的方法只提供一个实现类,则对于调用此方法的代码,也可进行方法内联。
冗余消除:在编译期,根据运行时状况进行代码折叠或消除。
server 又称C2,采用大量的技巧进行优化,占用内存较多,使用于服务器的应用,C2的优化是在逃逸分析的基础上,在编译时会做标量替换,栈上分配,同步消除等优化。
0.逃逸分析
注意:逃逸分析并不是一个直接的优化手段,是一个代码分析,分析变量的作用域来决定对象是在栈上还是在堆上进行分配,同时,基于逃逸分析,可以进行标量替换,栈上你分配和同步消除等优化。
什么是逃逸?逃逸即当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。 具体指全局变量赋值,方法返回值,实例引用传递。
逃逸分析即当对象是逃逸的,则在堆上分配对象,如果对象不是逃逸的,则在栈上分配对象,为什么要做逃逸分析?是为了减轻GC的压力,当对象没有发生逃逸,则在栈上分配对象,当方法结束后,则会自动释放对象所占的内存,故不需要将对象放在堆上,进行GC,从而减轻JVM的压力。
因此:当对象不再被其他对象使用的时候,尽量在方法中创建对象。
1.标量替换:用标量替换聚合量。当创建的对象并未用到其中的全部变量,则可以节省一定的内存,对于代码执行而言,不用去寻找对象的引用,则会更快一些。
2.栈上分配:如果对象没有逃逸,则直接在栈上创建对象,而不是在JVM的堆中。
3.同步消除:如果发现同步的对象未逃逸,则就没有同步的必要了,在C2编译时会直接去掉同步。
逆优化:即当运行C1,C2编译后的机器码如果不符合优化条件,则会进行逆优化,也就是回到解释执行的方式。eg基于类层次分析编译的代码,当有新的相应的接口实现类加入时,就执行逆优化。
默认情况下,Sun JDK根据机器来选择server或client,当机器配置CPU超过2核,且内存超过2GB,默认server模式,32为Windows始终是clinet,当然也可以强制。
注意:C2这种方式是根据运行状态来进行动态编译。
8.反射:反射可动态调用某对象实例中对应的方法,访问查看对象的属性等,反射和直接创建对象实例,调用方法的最大不同在于创建的过程,方法调用的过程是动态的。
9.JDK将内存空间划分为方法区,堆,本地方法栈,JVM方法栈,寄存器。
方法区:存放要加载的类的信息,类中的静态变量,类中定义为final类型的常量,类中的Field信息,类中的方法信息,eg:Class对象的getName,isInterface方法获取信息的来源是方法区。方法区域也是全局共享的,这块区域又称持久代,默认最小16M,最大64M, 通过 -XX:PermSize -XX:MaxPerSize指定最小最大值。
堆:用于存放实例对象以及数组值,Java中所有通过new创建的对象的内存都在此分配。32位操作系统上最大为2G,64为系统上则没有限制,大小可通过-Xms 和-Xmx来控制。
JDK1.2会后对堆采用分代管理的方式:
1.新生代:新生代又分为一个Eden区域,和两个Survivor区域,可通过-Xmn参数来指定新生代的大小,-XX:SurvivorRatio来调整Eden space和Survivor space的大小。SurvivorRatio= Eden/Survivor
2.老年代(旧生代):用于存放新生代中经过多次垃圾回收仍然存活的对象,也可以直接将对象分配到老年代中,主要有两种对象:一种是大对象,一种是大的数组对象。
本地方法栈:支持native方法的执行,
程序寄存器:存储即将执行代码的行号,
JVM方法栈:即用户方法的执行,JVM方法栈占用的为操作系统内存,JVM方法栈为线程私有,当方法执行完毕,栈帧也会被释放,当JVM方法栈的空间不足时,会抛出StackOverflowerError错误。
GC算法:复制算法,标记清除,标记压缩。
对象存活的判断:
1.引用计数:即当对象被引用,则引用计数器加1,如果计数器为0,则认为没有引用,但是对于循环引用,无法判断;
2.可达性分析:根据对象与根对象是否有关联,即可以到达根对象,这里的根对象是指:当前运行线程栈上引用的对象,常量,静态变量,没有被本地方法释放的对象引用。
对象引用关系:强引用,软引用,弱引用,虚引用,
新生代GC:
首先,新生代是为了存放存活时间较短的对象,因此选择复制算法来对新生代进行对象的回收,因为复制算法,在进行对象回收时需要一块未使用的空间来存放存活的对象,因此新生代被划分为Eden,survivor1,survivor2空间,Eden用于存放新创建的对象,s1,s2中的一块用于在Minor GC触发时作为复制算法的备用空间,因此,s1,s2中,总会有一个是空的。S0,S1通常又称From Space 和To Space。Sun JDK提供串行GC,并行GC,并行回收GC三种方式来回收新生代对象占用的内存,对新生代对象占用内存的回收又称Minor GC。
串行(Serial)GC:SurvivorRatio的值对应为eden space / survivor space ,survivorRation默认为8。新生代分配内存采用的为空闲指针的方式,指针保持最后一个分配对象在新生代内存区间的位置,当有新对象要分配内存时,需要检查剩余的空间是否能够存放新的对象,如果可以,则更新指针,创建对象,如果不可以,则触发Minor GC。
即GC先从根集合扫描出存活的对象,但当老年代中的对象引用新生代中的对象时,因为老年代比较大,为了不去扫描整个老年代,Sun JDK采用remember set的方式来解决这个问题,即当对对象进行赋时,如果发现赋值的是一个对象的引用,则产生write barriber,然后检查赋值对象是否在老年代即赋值对象的引用是否指向新生代,如果满足,则在remember set上做标记。因此完整的根集合对象为跟集合对象加上remember set中标记的对象,在确认根集合对象后,即可进行扫描来寻找存活的对象,但是在进行Minor GC的时候,防止引用关系发生变化,Sun JDK采用暂停应用的方式,即在编译的时候为每段方法注入SafePoint,SafePoint位于方法循环的结束点以及方法执行完毕的点,当用户线程进行SafePoint点,如果要执行Minor GC,则将内存也设置为不可读状态,从而暂停用户线程的执行。
当扫描完存活对象后,会将存活对象赋值到为空的s1或s2,然后清除不为空的s1或s2和eden区,当再次进行Minor GC的时候,同理会将存活对象赋值到刚才相反的空的s1或s2中,因此,每次都有一个空的s1或s2区用来进行复制算法。通常在Minor GC后并不是直接进入老年代,而是经历过几次Minor GC仍然存活的对象才放入老年代,存活次数通过-XX:MaxTenuringThreshold来设置,当存放数据的s1或s2空间满后,剩下的存活对象直接转入老年代。
通过-XX:+ UseSerialGC的方式来强制指定使用串行GC;
并行回收GC:
采用并行回收GC,Eden,s1,s2默认情况下的划分比例采用InitialSurvivorRatio值为8,InitialSurvivorRatio = 新生代大小(Xmn)/ survivor。可以通过该表该值来调整分区的大小。JDK1.6以后,也可通过-XX:SurvivorRatio来调整,SurvivorRatio = Eden / survivor,但并行回收会将此值+2赋给InitialSurvivorRatio,,当同事配置这两个属性参数,则以InitialSurvivorRatio为准。eg:-Xmn为16MB时,设置nitialSurvivorRatio,则Eden大小为12M,survivor大小为2M,如果只设置SurvivorRatio时,则Eden为12.8M,survivor为1.6M,为保持统一,建议设置SurvivorRatio参数。对于并行回收GC,启动时Eden,s1,s2的分配方式是按照survivorRation或InitialsurvivorRation来进行分配,但运行一段时间后,并行回收会根据Minor GC的频率,消耗时间来动态的调整Eden,s1,s2的大小,也可通过-XX:-UseAdaptiveSizePolicy来固定Eden,s1,s2的大小。
对于并行回收,并不是根据-XX:PretenureSizeThreshold来决定对象是否在老年代上直接分配,而是当需要给对象分配内存时,eden空间不够,如果此对象的大小等于eden一半的大小,就直接在老年代上分配。
并行回收GC,适合多cpu,对暂停的时间要求过短,可通过-XX:+UseParallelGC来强制指定。
并行GC:
基于SurvivorRatio值划分Eden,s1,s2,和串行GC一样。与并行回收的区别是并行GC必须配合救生带使用CMS GC,CMS GC在进行老年代GC时,有些过程是并发的,
可通过-XX:+UseParNewGC来强制指定。
老年代(旧生代)和永久代(持久代)可用的GC:
JDK提供了串行,并行,并发三种GC来对老年代和永久代对象所占用的内存进行回收:
1.串行:采用标记清除和标记压缩方法进行回收,即先从根集合对象开始扫描,对存活对象进行标记,然后遍历整个老年代和永久代空间,找出未标记的对象,并回收内存,最后执行滑动压缩,将存活的对象向老年代的开始空间进行滑动,留出连续的未使用空间。
2.并行:采用标记压缩算法进行回收,首先将空间划分为并行线程个数的区域(region),然后根据集合可访问到的对象以及并行线程的个数进行划分,并行的对这些对象进行着色(标记),当对象被着色,同时更新其所在的region存活的大小以及存活对象所在的位置,经分析,然后,GC从最左边开始往右扫描regions,直到找到第一个值得进行压缩移动的region,并将此region左边作为密集区域,对这些区域不做回收,然后继续往右扫描,对右边的regions根据存活的空间来决定压缩移动的源region和目标region,切换引用指针,同时清除region中没有标记的对象。
3.并发(CMS: Concurrent Mark-Sweep GC):CMS采用标记清除算法,在清理结束后,会产生很多内存碎片,造成内存不连续,因此CMS采用free list的方式来记录老年代空间中哪些部分是空闲的,当有对象需要在老年代分配内存时,便先去free list中寻找可以容纳对象的区域存储对象,多数情况下,老年代分配内存的请求都来源于Minor GC阶段,CMS分配老年代的方式导致Minor GC的速度下降;同时由于并发执行,有可能会造成free list竞争激烈,CMS引用Mutual exclusion locks,以JVM分配内存优先。
CMS进行回收的过程如下:
⑴第一次标记:暂停整个应用,扫描从根集合到老年代可直接访问的对象,并对这些对象进行着色,对着色的对象使用外部bit数组进行记录。
⑵并发标记:初始标记结束后,恢复应用的所有线程,然后并发的对之前着色的对象进行轮询,来标记这些对象可访问的对象。
⑶重新标记:暂停整个应用,因为Concurrent Marking可能会修改对象的引用关系或创建新的对象,因此需要重新对改变或新建的对象进行扫描,并进行重新着色
⑷并发收集:恢复所用应用,将没有标记的对象进行回收。
Full GC:当老年代和永久代触发GC时,对新生代,老年代,永久代都进行GC,又称Full GC,当Full GC被触发时,首先是进行新生代的GC,然后是对老年代和永久代进行GC。特殊情况:在发生Minor GC 之前, 可能Minor GC移动到老年代的对象多于老年代的剩余空间,这种情况下,Minor GC就不会执行,而是直接采用老年代的GC方式来对新生代,老年代,永久代进行回收。
执行Full GC的几种情况:
1.老年代空间不足;
2.永久代空间不足;
3.调用System.gc.
4.Minor GC晋升到老年代的平均大小大于老年代的剩余空间。
G1收集器:
JVM自带的分析工具:
JConsole用于图形化查看JVM内存的变化情况;
JVisualVM:查看内存消耗,线程的执行情况,程序中消耗的CPU,内存的动作;
JMap:JDK自带的分析JVM内存状况的工具;
JHat:JDK自带的一个用于分析jvm堆的工具;
JStat:JDK自带的一个统计JVM运行状况的工具;
Java线程同步机制:
eg:方法中的i++;
注意:i++操作不是一步完成的,对于赋值操作 a = 1+2; 这种操作时一步完成,是原子性的,而对于i++,它涉及到多步,因此存在并发下的不同步问题。
注意,每个线程启动后,都会被分配一个working memory区域(通常是操作数栈),i++在JVM中分为装载i,读取i,进行i+1操作,存储i,写入i,5个步骤。在这里就会存在两个问题:⑴working memory中i的值和main memory中的i值同步是否需要时间,⑵i++是由多个操作组成,只要多个线程在同一时刻同时执行操作,就会出现获取i值相同的现象。
保证同步的机制就是使用锁,注意:volatile关键字,是用来控制线程中对象的可见性,但并不能保证在此对象上操作的原子性,在i++中,将i定义为volatile是没有用。对于定义为volatile的变量,线程并不会将其从main memory复制到work memory中,而是直接在main memory中进行操作。
如果异议,敬请指出,谢谢,与君共勉。