Java学习笔记
编译器的静态绑定和动态绑定。
以下程序的输出是father还是son?
class father{
private String name = "father";
public String getname(){return name;}
}
public class T1 extends father {
private String name = "son";
//public String getname(){return name;}
public static void main(String[] args) {
T1 s = new T1();
System.out.println(s.getname());
}
}
在继承体系中,如果出现方法覆盖,则方法调用会执行子类的方法。但是,对于出现同名成员变量,则不存在覆盖的说法。你可以想如果删除了父类中name的声明,则父类中的getname一定会出现编译错误。以下的代码可说明问题。
class Father{
String name = "father";
String getname(){return name;}
}
public class Son extends Father{
String name = "son";
String getname(){return name;}
public static void main(String[] args) {
Father f = new Father();
System.out.println(f.getname()); //father
Son s = new Son();
System.out.println(s.getname()); //son
Father fs = new Son(); //声明类型为Father, 是静态绑定参考类型。 实例类型是Son,是运行时动态绑定参考类型。
System.out.println(fs.getname()); //son
Son sf = (Son)(fs);
System.out.println(sf.getname()); //son
}
}
静态绑定,就是左边引用的类型,他是用来在编译阶段控制访问用的,编译器用这个声明类型检查该对象所有的变量和方法调用是否合法。如果声明类型是实例类型的基类,则可能发生窄化。各种绑定发生的场景如下表:
绑定类型 | 待绑定对象 | 绑定到 |
---|---|---|
静态绑定 | Static变量 | 所属类 |
静态绑定 | Static方法 | 所属类 |
静态绑定 | 类内基本类型变量(堆中分配) | 所属类 |
静态绑定 | 方法内基本类型变量(栈中分配) | 所属类 |
静态绑定 | 任何引用型变量 | 声明类型 |
静态绑定 | Final方法 | 所属类 |
静态绑定 | Private方法 | 所属类 |
动态绑定 | 子类重写的父类方法 | 子类 |
备注:想要动态绑定的重写向上转型要确保真实发生重写,可以使用@Override验证。重写的两个关键:父类的被重写方法不是private和final,子类重写的格式符合规范。
获取当前类名的getClass().getName()方法。
写这个是自己做题时中过好几次陷阱了,实在是记了又忘,忘了又记。
class Father {
}
public class Son extends Father{
public static void main(String[] args) {
Son s = new Son();
Father f = new Son();
System.out.println(s.getClass().getName() + " " + f.getClass().getName());
}
}
//Output: Son Son
其实只要想想就知道了,这个方法是一个实例方法,即完整的方法调用是 this.getClass().getName()
。想想调用一个实例方法,会经过多态的向下查找过程,返回的是子类的类型就很正常了。
时曾相识的逗号表达式和局部静态变量
以下代码块在Java中有什么问题?
int a=1,b=2,c;
c=(a,b);
根据记忆中的逗号表达式,你会下意识的认为c=b,然后你会转念想,Java中到底支不支持逗号表达式?你的脑海中模糊的记得在教科书上的第二章运算符部分上赫然写明逗号表达式拥有很低的优先级,但是你又出现疑问,自己重来没有看过有谁或者开源库使用过逗号表达式,此时你纠结了。
以上的写法在Eclipse中会报“The primitive type int of a does not have a field b”错误。而去掉括号也是不对的,编译器会抱怨说把‘,’换成‘.’。
这里看错误提示,就知道Java根本不支持逗号表达式,编译器会认为你把成员引用'.'误写成为了','。根本没有考虑逗号表达式的语法解析。
有关静态变量的特性,如这个:
void callme(){
static counter=0;
counter++;
}
要知道,静态成员变量都是存储在持久区,一般Java中会把具有"唯一"性质的数据存在持久区中,例如类的类型信息,方法代码或者是Static对象,他们是具体属于一个类的信息,而且要随着类的Class对象在持久区的创建而创建,随着类Class对象的释放而释放。
而在C中的静态成员Static 写法,只不过是向编译器说明其位置和变量的作用域。Java中的Static变量只有一种作用域,就是类唯一的上下文。
异常语句,catch和throw和return。
如果在多层方法调用中,出现会覆盖return结果的内部throw异常,而进入到上层方法的catch中,此时又再次throw异常,会出现什么情况?
。。。。。。。。。。。。。未完待续
基本数据类型,new操作和包装类的valueof缓存问题。
。。。。。。。。。。。。。未完待续
变量初始化问题。
Java的静态成员变量,普通成员变量,final型变量,方法局部变量,提前初始化以及初始化中存在的有关问题。看下面的代码:
class C{
String a;
Integer i;
int []aa;
C c;
C(){
a= "abc";
}
void f(){
int a; //编译器报错
System.out.print(a);
}
final int b; //编译器报错
}
当类C因new操作在堆中(Eden)区建立时,此时是全0填充的。因此在对象分配后所有成员变量就是清零状态。零被基本数据类型和引用类型解释为自己的值,比如boolean初始化为false,int初始化为0,数组和引用都是null。
对于方法内部的变量,是必须要给值的,而且编译器会监督避免栈上脏数据的使用,编译器不负责栈上内存的初始化。
对于final类型的类成员变量,是必须在三种地方赋值的:
- 表达式声明赋值。
- 普通构造块。
- 构造方法。
再看下面的代码:
class Father {
static int a = Son.a; // 2
Son son = new Son(); // 8 COMMENT ME AND REPLACE WITH
static Son ss = new Son();
static{ // 3
System.out.println("Static Father " + a);
//System.out.println("Father " + son.b); //静态块不能访问实例对象
}
{ System.out.println("Father " + son.b); } // 9
}
public class Son extends Father{ // 1 7
static int a = 1;
int b = 1;
static { // 4
int i = 11, sum = 0;
while(i > 0){sum += i;i--;}
a=sum;
}
static{ // 5
System.out.println("Static son " + a);
}
{ System.out.println("son " + b); }
public static void main(String[] args) {
Son s = new Son(); // 6
}
}
Output:
Static Father 0
Static son 66
我们先来看一下,程序先从public方法执行开始,进行初始化Son类,然后发现有父类,递归进入父类开始初始化。首先获取Son.a,但是还没有执行子类静态初始化所以获取到的是0。然后在父类中打印a。在第 4 步才把Son.a计算出来,在第 5 步输出66。
开始执行 main 方法,首先会先到父类Father中执行初始化过程,在第 8 行注释时会开始创建Son实例,然后你会看到出现了在 Father.<init>
和 Son.<init>
之间的轮番抢夺,最后以栈溢出告终。
注释第 9 行,替换第 8 行为
static Son ss = new Son();
运行结果:
son 1
Static Father 0
Static son 66
son 1
看来只有静态初始化过程中能够提前访问子类未初始化的值,而实例初始化则不能。
这个例子说明,Java中对对象初始化采用递归是为了先让父类先初始化完成,然后供子类初始化或使用。所有出现提前访问到未初始化值的情况都因为存在一种途径使得可以访问到未初始化值。
总结一下,只要知道变量分配在哪里就知道其初始化值是什么,在堆上分配的经过内存管理器分配的返回给用户使用的开始都是清零的,然后执行方法区中的方法来初始化,当然,延伸一下,对于在持久区的静态变量在类加载时只初始化一次,在新生的变量会执行实例初始化的一系列流程。对于静态初始化的变量会保证原子性,因为JVM会保证静态变量只会初始化一次。加上final的都是要在对象new过程中立刻初始化的。带有‘立即意味,在编译阶段的编译器检查’一共有3种方法。但是要记住,final的引用逃逸问题源于java的3级重排序(字节码重排,处理器流水重排,内存重排),下面会说。
加上static的属于类的变量,只能在静态代码块中使用或初始化,如静态块,静态方法,,带有‘静态’的标签。
而static final,可以知道,是属于类的静态的需要立即初始化,即类的常量。
Java的JMM(内存管理模型)。
多线程环境中,对语句进行一系列重排序,插入屏障,final初始化的this指针逃逸,volatile(易变的)和锁的对比,自旋锁的适用范围
请参见《深入理解Java虚拟机》,《Java并行编程艺术》,《Java多线程编程实战》
垃圾管理相关
Java堆中一共可以分为3个代:年轻代,年老代,持久代。
并且执行2种GC清理:scavenger gc(即MinorGC)和FullGC。
前者当年轻代(Eden区)满时,使用Serial,parNew,等收集(或叫复制)算法摘出存活对象,放到另一个Survivor(叫做to区域)中,而另一个Survivor(叫做From区域)根据年龄决定对象去向,然后From和To交换角色。这样,Eden和一个Survivor为空。使用收集算法而不是整理是因为年轻代中临时对象比较多,并且存活对象分散。当大量的new操作时,会引发这种gc。
可以调整年轻代的比例布局。
Java也支持定义一个阀值,让新建的超大对象直接进入年老代。一般来说,只有当对象在Survivor中存在足够次数的MinorGC后,才会被移入年老代。
full gc是对整个内存进行整理。当:system.gc显示调用,或者如hibernate,其他java类库动态生成类类型时,会在持久代大量生成类信息。老年代满。持久代满。内存布局发生改变。
这里判断对象是否存活,使用的是根引用判断,即从任意一个可访问的对象出发,对于遍历不到的对象就是要收集的对象。这种方法的好处是防止引用计数算法不能释放循环引用的对象。
持久代包含类信息和类代码,因此又叫方法区。
一个类从内存彻底释放:先释放类代码区域,然后检查是否有类类型引用,即类元数据是否存在使用现象,如果没有才释放元数据,这时才完全释放该类。
1)-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
2)-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
3)-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。
一些自己的理解:对于Java中的栈和堆,只要是能够动态分配和释放的内存都可以叫堆,所以说从这个层面上上面的**代,包括方法区(是不是又叫持久代)都是堆区的。但是有的时候为了说明方法区的特殊性,则只把年轻代和老年代叫做堆。
而栈只存放私有的数据,例如基本数据类型,调用上下文数据还有可执行代码(这个是什么?)
不行了我还要看两遍深入理解Java虚拟机。!!
预求值特性。
以下的语句中,哪句会出现错误?
final byte a = 1,b = 2,c=a+b; //1
byte z; //2
byte x=a+b; //3
byte d=z+b; //4
byte e=a+2; //5
byte i=2*2*1000+1; //6
byte j=d+6; //7
byte k=x+=2; //8
解析:这里面,编译器会先进行预求值动作,将可以在编译前就能确定的值进行计算,然后在此基础上进行类型检查。
1中,变量c可以直接求出,且没有超出[-128,127],因此正确。
3中,虽然不是final型的,但是也可以直接求值。
4中,z是一个运行时刻的值,因此编译器在这里进行类型提升(对于不能直接求值进行类型检查和范围判断的表达式,将byte,short,char会提升到int类型),因此,第4句会出现类型强转错误。
5中,可以直接求值。
6中,很明显,超出了byte型的范围,编译器计算后是一个int型的值,也会发生类型强转错误。
7中,同4的类型错误。
总结,编译器首先会将能求值的都一次性求值了,然后查看右值是否与左面变量类型兼容,如果兼容(范围和类型方面),则正确;否则,编译器将其转换为int型尝试赋值,如果范围不丢失精度,则通过检查。例如将6改成short型就正确。
编译器自动求值发生在编译前期,因此算是编译器提供的语法糖,其他的语法糖还有,装箱和拆箱,String 类型直接使用字符串赋值。
继承结构中类的初始化过程。
还是老方法,看代码
class Father {
static int fsi1 = 1;
String arg1 = "Father arg1";
{
System.out.println("Father block1 called.");
}
static {
System.out.println("Father Static block called.");
}
{
System.out.println("Father block2 called.");
}
static int fsi2 = 1;
String arg2 = "Father arg2";
Father() {
System.out.println("Father Constracturer called.");
}
}
public class Son extends Father {
static int ssi1 = 1;
String arg1 = "son1";
{
System.out.println("Son block1 called.");
}
static {
System.out.println("Son Static block called.");
}
{
System.out.println("Son block2 called.");
}
static int fsi1 = 1;
String arg2 = "arg2";
Son() {
System.out.println("Son Constracturer called.");
}
public static void main(String[] args) {
new Son();
}
}
来看看执行结果:
Father Static block called.
Son Static block called.
Father block1 called.
Father block2 called.
Father Constracturer called.
Son block1 called.
Son block2 called.
Son Constracturer called.
解析:这就说明,在继承体系中,实例化动作是自顶向下的。一共分为2个级别的调用:Static级别 -> 对象实例级别 。在一个类中,Static变量、Static代码块地位平等,执行从上到下。而对象实例级别中,成员变量,普通代码块地位平等, 然后是构造方法的调用,即构造方法是在第二个级别中是最后一个。其实结合类的结构就知道这么调用的原因了。
若将代码添加一行,如以下
class Father {
static Son ss = new Son();////////////////////////////////////////////////
static int fsi1 = 1;
String arg1 = "Father arg1";
{
System.out.println("Father block1 called.");
}
static {
System.out.println("Father Static block called.");
}
{
System.out.println("Father block2 called.");
}
static int fsi2 = 1;
String arg2 = "Father arg2";
Father() {
System.out.println("Father Constracturer called.==》》 " + fsi2);
}
}
public class Son extends Father {
static int ssi1 = 1;
String arg1 = "son1";
{
System.out.println("Son block1 called.");
}
static {
System.out.println("Son Static block called.");
}
{
System.out.println("Son block2 called.");
}
static int ssi2 = 1;
String arg2 = "arg2";
Son() {
System.out.println("Son Constracturer called. ==》》 " + ssi2);
}
public static void main(String[] args) {
new Son();
}
}
则父类中会进入到并未进行静态初始化的自身和子类中构造对象,即可以形容的说为“静态初始化事件被搁置”,此时类已经在方法区初始化完毕(即内存清零),因此此时获取的是0值或者null值。
多线程环境中的volatile变量和synchronized锁的碎碎念。
我们都知道,volatile修饰的变量在每次读和写的时候会用实际内存刷新自己的工作内存(各线程接触不到主内存),并且作废其他cpu的直接CACHE中的CACHE行,(而不是CPU上下文。我以为是CPU上下文。上下文和这个无关,CPU上下文是一组寄存器值并且会写入到主内存,而volatile是封锁多CPU各自相同数据的缓存行)。
synchronized可以作用在变量,方法或者语句块,如果修饰方法,则持有的锁是类对象(即class对象)。如果修饰语句块,则需要传入一个对象。如果是变量,则会自动在访问该变量处增加同步锁。可以与wait和notify使用(注意wait和Thread.sleep的区别,一个释放监视器,一个不释放。前者只能使用同一个对象(即在synchronized块中)的notify、notifyAll被唤醒或者超时唤醒加入等待队列,后者可以使用interupt或者自己超时加入等待队列)。wait释放监视器后一直阻塞,而所有动作(包括继续执行下一条指令,或者是相应异常,例如使用interrupt方法引发的InterrupedException)都要在获取监视器成功后才能相应;相比而言,Thread.sleep即使在无限期阻塞中也还是能够相应终端异常。(因此,这提供了一种优雅的线程退出方法)这是Object类中原生定义的方法。
wait和notify 是 suspend和resume 相比较可以不发生死锁,因为在线程调用wait时会释放监视器。
Thread.sleep和interrput 或者 标志位 是常用的线程终止方法,而不是暴力terminate。
总结一下volatile和synchronized的异同点。
首先,volatile是弱版的synchronized,他保证一致性,但是不保证原子性。原子性需要例如无ABA问题的CAS,或者synchronized等其他措施的保证。
根据《Java多线程编程艺术》,各种CPU架构和编译器实现为了执行优化和指令流水线保持高流量都有各自的源码级别和指令级重排序,因此会造成读后写,写后读,写后写等问题。对于单线程来说,JMM执行重排序保证得到正确的结果,即可以说是“你不用管我是怎么运行的,结果肯定正确就是了”。即给程序员一种串行执行的幻觉。但是在多线程情况下,线程和指令执行都是乱序,JMM使用内存屏障保证程序员期望的执行顺序约定。例如volatile,线程执行,synchronized,方法执行完成,。。。想不起来了啊啊啊。
重排序一般都是对没有数据相关的执行重排序。但是在多线程环境下可能出现数据相关以及控制相关,即处理器的分支预测,提前执行机制。此时会出现if语句体在if判断之前执行。
重排序即使在单线程中也是存在的,网上的一些博客写的是重排序在单线程中不存在,这点我是不接受的。
synchronized实际上,粒度比volatile大,可以保证原子性,是以封锁总线
synchronized对类的class对象或者对象的加锁涉及到锁的一系列变化,从而出现一系列概念,比如轻量锁,重量锁,锁膨胀,锁粗化,自旋等待,偏向锁, lock类等概念,
。。。。。。。。。。。。。未完待续
详见Java内存模型。请牢记三个方面: 原子性(例如32位jvm中对64位变量的非原子性读写实验,见如下),有序性,可见性。
对于多线程环境下的CACHE行,有一个很经典,很巧妙的优化。是来自冰法编程大师Doug Lea的一个优化。
比如在32位系统中,CACHE行大小假设为64B。而有一个类,成员声明的是队列的队头和队尾引用,且有可能是多线程操作因此都是volatile类型。因为是引用类型,因此占4B,即一共占8B。这样,当把对象读到CACHE中时,这两个对象就会在一个缓存行中。当对队列引用进行改写时,其他所有CPU的缓存行都会被封锁,这样就会限制队列的双向操作。因此可以在类的成员变量中添加(64-8)/4=14个Object引用。让整个对象大小超出一个缓存行的大小,让队头和队尾引用在两个缓存行中,可以提升多线程并行性能。该方法有一种缺点,即填充变量可能会被java编译器或者JVM优化掉。
。。。。。。。。。。。。。未完待续
final类型在构造方法中的this指针引用逃逸问题。
。。。。。。。。。。。。。未完待续
重载带来的类型自动提升和强制窄化。
对于重载方法根据参数类型调用的选择问题,如果是char型,有char类型的方法则选择该方法,否则直接提升到int类型;而整形会按照一级一级的阶梯式自动提升。
对于使用java.exe执行的字节码文件,含有main方法的类名只需要和文件名相同即可运行,即使不是public也可以。
几个关于内存调优的资料:
JVM系列三:JVM参数设置、分析
Frequently Asked Questions About the Java HotSpot VM
JVM 几个重要的参数