java反射机制以及在android开发中的应用

java中的反射机制:只要给定类的名字就可以得到所有类的信息。因为这个类的名字是可以在代码运行时动态指定的,所以利用java的反射机制比通过new的方式要灵活的多

在java中,通过new的方式创建的对象称为静态加载(编译时加载类),通过反射机制创建对象称为动态加载(运行时加载类)

java反射机制的使用方式

这里有个Human类如下

package com.myway5;

public class Human {
private String name;
private int age;
public String word;

public Human() {
    // TODO Auto-generated constructor stub
}

public Human(String name) {
    this.name = name;
}

public Human(String name, int age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}
public void speak(String word) {
     System.out.println(word);
 }

}
我们使用反射机制获取Human类所有的信息

package com.myway5;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Client {
public static void main(String[] args) {
// 获取类名等信息
Class class1 = Human.class;
System.out.println(class1.getName());
System.out.println(class1.getSimpleName());

    System.out.println("***********构造方法**********");

    Constructor[] cs = class1.getDeclaredConstructors();
    for (Constructor constructor : cs) {
        System.out.print(constructor.getName() + "(");
        Class[] paramsType = constructor.getParameterTypes();
        for (Class class2 : paramsType) {
            System.out.print(class2.getName() + ",");
        }
        System.out.println(")");
    }

    System.out.println("**********公共成员变量************");
    Field[] fields = class1.getFields();
    for (Field field : fields) {
        System.out.println(field.getType().getName() + ":" + field.getName());
    }

    System.out.println("**********公共方法**************");
    Method[] methods = class1.getMethods();
    for (Method method : methods) {
        System.out.print(method.getName() + "(");
        Class[] params = method.getParameterTypes();
        for (Class class3 : params) {
            System.out.print(class3.getName() + ",");
        }
        System.out.println(method.getReturnType().getName() + ")");
    }

}

}
输出结果如下:
com.myway5.Human
Human
***********构造方法**********
com.myway5.Human(java.lang.String,int,)
com.myway5.Human(java.lang.String,)
com.myway5.Human()
**********公共成员变量************
java.lang.String:word
**********公共方法**************
getName(java.lang.String)
setName(java.lang.String,void)
getAge(int)
setAge(int,void)
wait(long,int,void)
wait(long,void)
wait(void)
equals(java.lang.Object,boolean)
toString(java.lang.String)
hashCode(int)
getClass(java.lang.Class)
notify(void)
notifyAll(void)
可以看到,所有公共的成员变量,方法都可以通过这个方式获取到,那么怎么调用其中的方法呢
public static void useMethod() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
InstantiationException, NoSuchMethodException, SecurityException {
Class class1 = Human.class;
Human man = (Human) class1.newInstance();//通过newInstance创建实例
Method method = class1.getMethod(“speak”, String.class);
Object object = method.invoke(man, new Object[] { “hello reflect” });
}

android开发中的使用后续更新

继上文更新:

android开发中常常会使用java的反射机制来更改一些系统底层无法更改的代码逻辑。比如说在AlertDialog的使用中,通过自带的setPositiveButton或者setNegativeButton时,一旦点击按钮都会退出dialog,有时候我们不希望他退出,比如用户登录时登录失败再次登录。那么一种解决办法 就是通过java的反射机制。

package com.myway5.java_reflect;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;

public class MainActivity extends AppCompatActivity{

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    AlertDialog.Builder builder=new AlertDialog.Builder(this);

    builder.setMessage("Hello Dialog")
             .setTitle("对话框")
                .setView(getLayoutInflater().inflate(R.layout.dialog_signin,null));
    builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

        }
    });
    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

        }
    });
    AlertDialog dialog=builder.create();
    try {
        Field field = dialog.getClass().getDeclaredField("mAlert");
        field.setAccessible(true);
        Object obj=field.get(dialog);
        field=obj.getClass().getDeclaredField("mHandler");
        field.setAccessible(true);
        field.set(obj,new ButtonHandler(dialog));
    }catch (Exception e){
        e.printStackTrace();
    }
    dialog.show();
}
private static final class ButtonHandler extends Handler {
    // Button clicks have Message.what as the BUTTON{1,2,3} constant
    private static final int MSG_DISMISS_DIALOG = 1;

    private WeakReference<DialogInterface> mDialog;

    public ButtonHandler(DialogInterface dialog) {
        mDialog = new WeakReference<DialogInterface>(dialog);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {

            case DialogInterface.BUTTON_POSITIVE:
            case DialogInterface.BUTTON_NEGATIVE:
            case DialogInterface.BUTTON_NEUTRAL:
                ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
                break;

            case MSG_DISMISS_DIALOG:
                //这里是点击后的逻辑,因为注释掉了dismiss(),dialog不会退出了
                //((DialogInterface) msg.obj).dismiss();
        }
    }
}

}
其中
try {
Field field = dialog.getClass().getDeclaredField(“mAlert”);
field.setAccessible(true);
Object obj=field.get(dialog);
field=obj.getClass().getDeclaredField(“mHandler”);
field.setAccessible(true);
field.set(obj,new ButtonHandler(dialog));
}catch (Exception e){
e.printStackTrace();
}
这个部分就是通过反射获取AlertDialog的私有变量mAlert,然后获取mAlert的成员变量mHandler,将这个Handler设置成我们自己的ButtonHandler,这样就解决了点击后退出的问题

代码地址:https://github.com/joyme123/java_reflect

参考文章:http://www.oschina.net/question/163910_27112

java常量池

package 测试常量;

public class Test {
public static void main(String[] args){
String s1 = “hello world”;
String s2 = “hello world”;
System.out.println(s1==s2);

String s4 = new String(“你好”);
String s3 = “你好”;
System.out.println(s3==s4);

Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2);

Integer i3 = 2000;
Integer i4 = 2000;
System.out.println(i3 == i4);

Integer i5 = new Integer(10);
Integer i6 = new Integer(10);
System.out.println(i5 == i6);

int i7 = 2000;
int i8 = 2000;
System.out.println(i7 == i8);

Double f1 = new Double(1.01);
Double f2 = new Double(1.01);
System.out.println(f1 == f2);

Double f3 = 1000.01;
Double f4 = 1000.01;
System.out.println(f3 == f4);

double f5 = 1000.01;
double f6 = 1000.01;
System.out.println(f5 == f6);
}
}


输出结果是

true
false
true
false
false
true
false
false
true

JavaEE post Base64的图片丢失数据解决

项目中有个提交Base64编码的图片到服务器,服务器端转换成图片的二进制流存放到数据库中。在实际使用中发现,保存到数据库中图片文件有损坏。表现为前一部分二进制数据相同,到了后面就开始不同了。

一开始的步骤为:

st=>start: 准备Base64数据
op1=>operation: Post准备好的Base64数据
op2=>operation: 将数据并转换为byte[]
op3=>operation: 存储到数据库
e=>end

st->op1->op2->op3->end

为了找出问题,将post这个步骤省去,并将数据直接存成图片以便观察

st=>start: 准备Base64数据
op2=>operation: 将数据并转换为byte[]
op3=>operation: 存储成图片
e=>end

st->op2->op3->end

这一步保存的文件的大小是9.0k

st=>start: 准备Base64数据
op1=>operation: Post准备好的Base64数据
op2=>operation: 将数据并转换为byte[]
op3=>operation: 存储成图片
e=>end

st->op1->op2->op3->end

这一步保存的文件的大小是8.9k

上面两步对比可以说明数据在传输的时候出现了丢失的情况,于是我猜测是不是tomcat对post方式做了限制,发现不是。查询资料发现,Base64位数据传输的时候中间的+号容易发生丢失。解决方法是将post过来的数据先UrlEncode一下即可

发生这个问题的没有找到明确的可靠的解释,其中一片文章解释为javascript将+当做字符串连接符号处理导致丢失。

Java虚拟机学习记录(九)——类文件结构(上)

##一、前言
在Java开发中,可以通过javac将.java的源代码编译为.class的类文件。之前一直以为,只有java语言可以编译为.class。但是在前些天的学习中,了解到不仅仅是Java语言可以编译成.class文件然后运行在Java虚拟机上,Clojure、Groovy、JRuby、Jython、Scala等语言都可以运行在Java虚拟机上。觉得这真是太神奇了。今天这一章的内容刚好可以解释这些。

##二、Java虚拟机的无关系特点
一般都说Java是平台无关的,因为Java是运行在Java虚拟机上的,而Java虚拟机是开发成各个平台通用的。那么同时,Java虚拟机其实也是语言无关的,也就是说,Java虚拟机并不要求特定的语言。只要该语言可以被编译生成符合标准的class(类)文件,那么就是可以运行在Java虚拟机上了。那么,类文件的结构是什么样的呢?

##三、类文件的基本知识
##1.基本单位
类文件是以8位字节为基础单位的二进制流,没有任何分割符。当遇到需要占用8位以上的数据时,则会按高位在前的方式分割成若干个8位字节进行存储。
###2.存储数据的数据格式:无符号数和表。
无符号数属于基本数据类型。以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节,8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者安装UTF-8编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性以_info结尾。

##三、类文件的结构
###1.魔数和版本号
class文件的前四个字节称为魔数(Magic Number),用来描述文件的格式,class文件的魔数是0xCAFEBABE。第五六个字节描述次版本号(Minor Version),第七八个字节描述主版本号。
###2.常量池
紧接着主版本号之后是常量池的入口。由于常量池的常量数量是变化的,所以在常量池的入口有一个u2类型(第9,10位)的数据,代表常量池容量计数值。不过这个计数是从1开始的,所以如果这个值是22,则代表有21个常量。
常量池中主要有两种类型:字面量(Literal)和符号引用(Symbolic References)。

a.字面量: 接近java语言的常量的概念,如文本字符串,声明为final的常量值等
b.符号引用:1.类和接口的全限定名(Fully Qualified Name) 2.字段的名称和描述符(Descriptor) 3.方法的名称和描述符

java代码在编译时不会像c/c++一样进行”连接”,这样在编译生成的class文件中不会保存各个方法、字段的最终内存布局信息,而是在运行的时候进行动态连接。也就是从常量池中获得方法、字段对应的符号引用,再在类创建或运行时解析翻译到具体的内存地址之中。

常量池中每一个常量都是一个表,在JDK1.7前共有11中常量,在JDK1.7中为了更好的支持动态语言调用,又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。

这14个表的共同之处在于,表开始的第一项是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMathodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

###3.访问标志
在常量池结束之后,紧接着的是访问标志,用来描述一些类和接口的访问信息,包括这个Class是类还是接口,是否定义为public,是否是abstract如果是类的话,是否是final。

###4.类索引,父类索引与接口索引集合
访问标志之后是类索引,父类索引与接口索引集合。类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据集合(对应java语言中的单继承和多接口实现),

###5.字段表集合
再之后是字段表集合。字段表(Field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。一个字段的描述包括:字段的作用域(public,private,protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型,数组,对象)、字段名称。

###6.方法表集合
字段表之后是方法表集合。很显然,对方法的描述和对字段的描述是很像的。volatile和transient不能描述方法,但是syncronized、native、strictfp和abstract是方法独有的。

Java虚拟机学习记录(八) —— 虚拟机性能监控与故障处理工具

一、前言

我觉得Java的强大之处在于它有非常完善的生态环境,从开发工具到分析处理工具。使用JDK中提供的工具可以在遇到程序故障时快速定位故障发生的原因并进行调优。

二、JDK命令行工具

a、jps(JVM Process Status):虚拟机进程状况工具

jps可以用来列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main class,main函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。

jps命令格式:
jps [option] [hostid]
使用范例:
jiang@jiang-HP-ENVY-Notebook:~$ jps -l
6084 /home/jiang/eclipse//plugins/org.eclipse.equinox.launcher_1.3.200.v20160318-1642.jar
6152 sun.tools.jps.Jps
jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。

选项 作用
-q 只输出LVMID,省略主类的名称
-m 输出虚拟机进程启动时传递给主类main()函数的参数
-l 输出主类的全名,如果进程执行的是Jar包,输出Jar路径
-v 输出虚拟机进程启动时JVM参数

b、jstat(JVM Statistics Monitoring Tool): 虚拟机统计信息监视工具

jstat是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、JIT编译等运行数据。

jstat命令格式为:
jstat [option vmid [interval [s|ms] [count]] ]
如果VMID是本地进程,和LVMID是一样的,如果是远程虚拟机进程,那VMID的格式是:
[protocol:][//]lvmid[@hostname[:port]/servername]
参数inerval和count代表查询间隔和次数,如果省略则表示只查询一次。
使用范例:
jstat -gc 2764 250 20
代表每250ms查询一次进程2764垃圾收集状况,一共查询20次

选项 作用
-class 监视类装载,卸载数量,总空间以及类装载所耗费的时间
-gc 监视Java堆状况,包括Eden区,两个survivor区,老年代,永久代等的容量、已用空间、GC时间合计等信息
-gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与-gcutil功能一样,但会额外输出导致上一次GC的原因
-gcnew 监视新生代GC状况
-gcnewcapacity 监视新生代使用到的最大、最小空间
-gcold 监视老年代GC状况
-gcoldcapacity 监视老年代使用到的最大、最小空间
-gcpermcapacity 输出永久代使用到的最大、最小空间
-compiler 输出JIT编译器编译过的方法、耗时等信息
-printcompilation 输出已被JIT编译的方法

Java虚拟机学习记录(七)——内存分配与回收策略

一、前言

Java的内存分配,从全局来看,就是堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接的栈上分配,对象的分配主要在新生代的Eden区上,如果开启了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中(大的对象直接进入老年代)。

二、对象优先在Eden分配

Java堆的新生代中,被分为Eden和两个survivor。大多数情况下,对象在Eden区优先分配,当Eden区没有足够的空间进行分配时,虚拟机将会发起一起Minor GC

新生代GC(Minor GC):指发生在新生代的垃圾回收动作。
老年代GC(Major GC/Full GC):指发生在老年代的GC。

三、大对象直接进入老年代

大对象指的是要占用大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象的内存分配堆虚拟机来说是一件很难的事,因为往往会因为找不到这样的连续的内存空间而不得不提前触发一次Full GC。因此,在写程序中要尽量避免这样大对象,尤其是生命周期很短的大对象。大对象的分配一般会直接进入老年代。(这里可以认为JVM是很不支持生命周期很短的大对象的创建的)。

四、长期存活的对象将进入老年代

虚拟机为每个对象定义了一个年龄计数器,在一次GC后仍然存活的对象它的年龄就会+1,如果对象在Eden出生并且经过第一次Minor GC仍然存活并且Survivor能够容纳就会被移到Survivor(其实就是标记-复制法)。当它的年龄达到一定的程度(默认为15岁),就会被移入老年代。可以通过-XX:MaxTenuringThreshold来设定这个年龄阈值。

五、动态年龄判定

为了更好地适应不同程序的内存情况,虚拟机并不是永远都要求对象的年龄必须达到了MaxTenuringThreshold才能进入老年代,如果在Survivor区中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代(节省了一半以上的Survivor内存)。

六、空间分配担保

在新生代的Minor GC是采用的标记——复制法,所以一次Minor GC后会将存活的对象移到另一个空闲的Survivor区中,但是没有人可以保证一次GC后存活的对象是Survivor能够容纳的,那么就需要老年代的进行空间分配担保,以防止在容纳不下的情况下有后备的解决方案。所以过程是:

Minor GC发生之前:检查老年代的连续空间是否能够容纳下新生代的所有对象,如果可以,则可以确保Minor GC是正常的。如果不可以,虚拟机就会检查HandlePromotionFailure设置值是否允许担保失败。如果允许,那么就会检查老年代最大可用连续空间是否大于历次进入老年代对象的平均大小,如果大于,则尝试一次Minor GC;如果小于,或者不允许担保失败,就要进行一次Full GC(所以可以理解为Full GC是为了给Minor GC腾出空间,所以Full GC之后往往跟随着一次Minor GC)。

Java虚拟机学习记录(六)——HotSpot算法实现

一、前言

在JVM运行的过程中,垃圾回收是性能提升的重中之重,垃圾回收的前提是准确判定哪些对象是可以回收的,在前面的学习中说到,Java的大多数虚拟机都是通过可达性分析算法来判定对象能否回收。那么如何去找这些根节点(GC ROOTS)的位置也是一个要解决的问题。

二、HotSpot枚举根节点(GC Roots)

根节点主要在全局性的引用(常量和类的静态属性)和执行上下文中(例如栈帧的本地变量表中),因为这些区域的占用内存往往很大,不可能去逐个检索,因为这个操作太耗时了。

同样的,可达性分析的时间严格要求还体现在GC停顿上,GC停顿就是指在可达性分析期间所有的Java执行线程得全部停下来,等分析完成之后再开始重新运行,如果不停顿,就可能导致分析期间,引用关系还在不断的改变。很显然,这个停顿时间必须短不然用户体验极差。Sun把这个停顿叫做Stop the world。

HotSpot采用一种叫OopMap的数据结构来记录所有的执行上下文和全局引用位置,这样就可以直接得到所有的GC Roots的地址了。

但是在Java程序运行过程中,有很多指令会导致对象的引用关系发生变化,如果每个变化都要写入到OopMap中,那样就需要维护一个很大的OopMap数据结构,会占用大量的空间。所以在Jvm中有一个安全点(SafePoint)的概念。HotSpot只在安全点处记录了这些信息,然后开始GC。安全点的选定不能太少造成GC等待时间过长,也不能频繁GC增加运行负荷。

如何让所有线程跑到安全点时停止下来,有两种方案可以选择,抢先式中断(Preemptive Suspension)主动式中断(Voluntary Suspension)

其中抢先式中断不需要线程的执行代码主动配合,在GC发生时,首先让所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程去主动轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外加上创建对象需要分配内存的地方。

但是安全点只能很好的解决运行中的程序,对于”不运行”的程序也就也就无法进入安全点,也就无法进行GC。这里的不运行指的是线程没有分配到CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程是无法响应JVM的中断请求的,”走”安全的地方去中断挂起,JVM显然也不太可能等待线程重新被分配CPU时间。这种情况需要借助安全区域(Safe Region)来解决。

安全区域指的是在一段代码中,引用关系不会发生变化。在这个区域任意开GC都是安全的。我们也可以把安全区域当成是安全点的扩展。

当线程执行到安全区域中时,首先标识自己进入了安全区域。在这段时间内JVM发起GC就不用管安全区域里的线程了。但是在安全区域内的线程重新获得CPU时间要离开Safe Region时,要检查系统是否已经完成了根节点枚举。完成了才能离开否则就要等待完成。

##三 HotSpot垃圾收集器的实现

a.Serial收集器

这是一款最基本,发展历史最悠久的收集器。这个收集器是一个单线程收集器,并且在收集垃圾时,必须暂停其他所有工作线程。适用于作为Client模式下的虚拟机。

b.ParNew收集器

其实就是Serial收集器的多线程版本。在单CPU的环境中,性能比不上Serial收集器,但是多CPU的时候性能是要好过Serial收集器的。

c.parallel Scavenge收集器

新生代收集器,复制算法,这个收集器与其他收集器的区别在于,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。吞吐量=运行用户代码所花费的时间/(运行用户代码时间+垃圾收集时间)。而其他收集则是关注减少GC停顿的时间。

d.Serial Old收集器

是Serial收集器的老年代版本,使用标记整理算法。也是给Client模式下的虚拟机使用。在server模式下,还有两大用途:1.在JDK1.5之前和Parallel Scavenge配合使用; 2作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure失败时使用。

e.Parallell Old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。配合paraller Scavenge使用。

f.CMS收集器

Concurrent Mark Sweep。以获取最短时间停顿为目标,基于“标记-清除”算法。包括四个步骤:
1.初始标记
2.并发标记
3.重新标记
4.并发清除
他有一下几个缺点:
1.对CPU资源敏感,对性能影响大
2.无法清理浮动垃圾
3.容易产生内存碎片,触发Full GC。

g.G1收集器

是一款面向服务器的垃圾收集器。有以下特点:
1.并行和并发
2.分代收集
3.空间整合
4.可预测的停顿

Java虚拟机学习记录(五)-垃圾收集器

一、前言

众所周知,Java与C的一个显著的区别在于c需要手动的去管理内存,而Java几乎不需要去做这样的处理。原因在于Java的虚拟机有一套自己的内存回收策略。

在Java虚拟机运行过程中,虚拟机栈、程序计数器、本地方法栈随着线程的生命周期的变化而变化,因此这一部分的内存是不需要额外的去回收。但是对于Java堆来说,几乎所有的对象的创建(这里之所以说几乎,是因为随着JIT编译器、对象逃逸分析和栈上分配的技术发展,部分对象不需要在堆中分配内存)都是在堆中进行,如果不作内存回收,很快就会被占满内存,那么怎么样去分析得到那些对象已经不再需要则是问题的关键。所以,要实现垃圾回收,先要判断对象是否已死,然后再对已死对象执行回收算法。下面记录几种常见的在垃圾回收中的算法

二、对象是否已死的判定——引用计数法

这种方法原理很简单,就是说每个对象增加一个引用就给他的计数器加1,当引用失效时(这里的引用失效,我觉得是说当程序的执行离开了对象的作用域,那么这个引用就算是失效了),计数器就减一。当计数器再次变为0时,这个对象就判定它已经可以回收了。

但是这种方法也有一个严重的问题,当ObjectA.instance = ObjectB;ObjectB.instance = ObjectA时,这里两个对象互相引用着导致引用计数一直不为0。因此在JVM的主流实现中,都不采用这种方法。据说python的GC算法就是引用计数加上辅助算法完成的。

三、对象是否已死的判定——可达性分析算法

可达性算法是借助树的数据结构的一个算法,通过判定一个对象是否可以通过树的根到达来确定其是否需要回收。引用一张来自网上的图片图片引用地址

可达性分析

在这里,虽然object 8,object 9,object 10,object 11,object 12都互相持有引用,但是因为从GC Roots中无法到达,所以可以判定为可回收对象。很好的解决了引用计数法的弊端。
在Java中,可作为GC Roots的对象包括:
虚拟机栈(栈帧中的本地变量表)中的引用对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象

四、垃圾回收算法——标记-清除算法

标记-清除算法共有两个阶段。标记和清除

标记是将内存空间扫描一遍,对所有可回收的对象做标记。清除是对内存空间再做一遍扫描,清除可回收的对象。这种方法有两个问题:一是要做两遍扫描,效率不高,二是会产生大量的内存碎片(回收的对象随机分布造成),当分配一个很大的对象时很有可能找不到这样的连续空间而提前触发一次垃圾回收。因此这种算法大多数的JVM的实现都不使用。

五、垃圾回收算法——复制算法

复制算法最大的特点是将内存空间分为大小相同的两部分。当开始垃圾回收时,只需将存活的对象移到另一块没有使用的内存空间,然后将使用的一边全部回收,这样的做法简单高效,但是对内存的浪费实在是太大了。就像是你买了8G的内存条只能使用4G,你肯定是不愿意的。

但是实际上,现在的商业虚拟机都采用这种算法的优化版本来回收新生代。因为在新生代中(Java堆会分为新生代[Young Generation]和年老代[Old Generation])中的对象绝大多数都是可回收的,那么实际上每次做垃回收时,新生代中存活的对象并不多。所以并不需要按照1:1进行空间分割,一般情况下,会将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和一块Survivor空间,当进行垃圾回收时,将这两块空间中的存活对象移到另一块空闲的Survivor空间。然后将对象全部清除。这样只有10%的内存会被浪费。

经历了几次垃圾回收都依然存活的对象会被放置到年老代中,因此年老代中的对象都是不容易被回收的。

因为没有办法保证每次的垃圾回收过程存活的对象都不超过10%,所以当Survivor空间不够用时,需要依赖其他内存(这里指年老代)进行分配担保(Handle Promotion)。

六、垃圾回收算法——标记-整理法

上面的复制算法很明显不适用于年老代,因为年老代中的对象特点是存活率大。标记-整理算法与标记-清除算法类似,但是它不是直接对对象进行清除,而是将存活的对象向一端移动,然后直接清理掉端边界意外的内存。引用一张来自网络上的图片图片引用地址

此处输入图片的描述

七、扩展一下引用的知识

在四、五的判定对象是否已死中,均涉及到了引用。在上面的表述中,似乎只有引用和死亡两种状态。但是事实上,Java规定了四种引用状态来帮助Java程序获得更好的性能。怎么去理解这个,一般来说,当Java虚拟机的内存足够时,有的对象虽然已经不需要了,但是完全没有必要将它丢弃,只有在进行垃圾回收后内存仍然不足时才将这些对象回收。这样就增加了这些对象的复用。免去了下一次使用这些对象又要重新创建的问题。很多系统的缓存功能都符合这样的应用场景。

在JDK1.2后,Java扩充了引用的概念,分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

a.强引用
Object obj = new Object()这种就是强引用,只要这个引用存在,无论如何JVM都不会回收这些对象

b.软引用
软引用用来描述一些还有用但并非必需的对象。用软引用的对象在系统进行过垃圾回收仍然内存不足时才会进行回收。在JDK1.2之后,提供了SoftReference类来实现软引用。

c.弱引用
弱引用也是用来描述非必需的对象,但是强度比软引用还弱一点。在下一次GC时必定会回收。

d.虚引用
虚引用对对象的生存时间构不成影响,就和没有引用与之关联一样,所以任何时候的GC都会将其回收。对一个对象设置虚引用的唯一目的就是这个对象被回收时会收到一个系统通知。在JDK1.2以后,提供了PhantomReference类来实现虚引用。

用代码来检验一下软引用、弱引用和虚引用。

package test;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;

public class TestReference {
    public static boolean run = true;
    public static void main(String[] args){
        WeakReference<String> weakReference = new WeakReference<String>(new String("weak Reference"));
        SoftReference<String> softReference = new SoftReference<String>(new String("soft Reference"));
        final ReferenceQueue<String> queue = new ReferenceQueue<String>();
        new Thread(new Runnable() {

            @Override
            public void run() {
                while(run){
                    Object object = queue.poll();
                    if(object != null){
                        try {
                            Field referent = Reference.class.getDeclaredField("referent");
                            referent.setAccessible(true);
                            Object result = referent.get(object);
                            System.out.println("即将回收"+result.getClass()+(String)result);
                        } catch (NoSuchFieldException | SecurityException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalArgumentException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }

            }
        }).start();
        PhantomReference<String> phantomReference = new PhantomReference<String>(new String("phantom Reference"), queue);

        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
        System.out.println("*****************下面开始GC******************");
        System.gc();        //System.gc只是建议系统进行垃圾回收,并不是立刻执行
        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
    }
}

上面这段代码的输出为

weak Reference
soft Reference
null
*****************下面开始GC******************
null
soft Reference
null
即将回收class java.lang.Stringphantom Reference

可以看到,软引用在gc之后仍然是存在的,而弱引用gc之后变成null了,虚引用一直为null。并且我们通过新开一个线程来检测虚引用被回收的通知。所以正确的使用soft Reference和weak Reference可以实现缓存和防止内存泄露,而虚引用一般用来实现细粒度的内存控制。比如下面代码实现一个缓存。程序在确认原来的对象要被回收之后,才申请内存创建新的缓存。Java幽灵引用的作用

八、总结

可以看出,不论是对java堆中内存空间进行分代,还是对引用进行四种类型的划分,都是为了解决在java程序运行过程中因为存在各种各样的对象,单一的垃圾回收算法没有办法高效的进行。事实上垃圾回收算法再不断的变化,每一种当前存在的垃圾回收算法都有它适应的运行环境。因此在什么时候使用什么样的垃圾回收算法,对于程序的性能来说也是至关重要的。

Java虚拟机学习记录(四)-对象的内存布局和访问定位

一、前言

jvm创建对象的过程分为类加载检查,分配对象空间,初始化类空间,设置对象信息,对象初始化。那么在分配对象空间时是如何分配的,怎么保证能够定位到对象的内存位置。

二、对象的内存布局

对象在内存中的布局可以分为三部分:对象头(Object Header),实例数据(Instance Data)和对齐填充(Padding)。

a.对象头

对象头有两部分,一部分是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向他的类元数据的指针(表示这个对象是哪个类实例化出来的)。并不是所有的虚拟机实现都必须在对象数据上保留类元数据的指针。因为查找对象的类元数据信息并不一定要经过对象本身(通过句柄)。另外,如果对象是一个Java数组,那在对象头中还必须要有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的类元数据信息确定Java对象大小,但是数据的类元数据中却无法确定数据的大小。

b.实例数据

存储对象中的各种类型的字段内容以及普通对象的指针(oops,Ordinary Object Pointers)。

c.对象填充

不是必然存在,没有特别意义,作为占位符存在。

三、对象的访问定位

建立对象是为了使用对象,对象的访问是通过栈上的reference数据来操作堆上的具体对象。reference是java虚拟机规范的一个指向对象的引用,但并没有规定如何去具体实现,一般来说,有两种实现方式:句柄和直接指针。

a.句柄

采用句柄方式会在Java堆中开辟出一块句柄池空间,Java栈中的本地变量表中存放着指向句柄池中某一个句柄的reference,然后句柄保存有指向某个实例的指针和指向方法区的对象类元数据。
reference---->句柄------>对象和类元数据,共三次指针定位
这种方式的优点是GC清理垃圾时会移动对象地址,栈中的reference不需要改变只需要改变句柄中指向对象的指针。

b.直接指针访问

reference---->对象实例数据(对象实例数据的对象头包含类元指针)------>类元数据,共两次指针定位
这种方式的优点是速度更快,节省了一次指针定位的时间。Sun HotSpot采用的就是这种对象的访问定位方式。

c.注意

在JDK1.8中,已经完全移除了方法区,类元数据的存储放在了本地内存中,这样就不会再收到方法区大小的限制。

Java虚拟机学习记录(三)-对象创建的过程

在Java程序运行时几乎每时每刻都有对象在被创建出来,从语言层面上看,只是new了一个对象,但是在虚拟机中这个对象的创建过程时比较复杂的(这里的对象仅适用于普通的Java对象,不包括数据和Class对象)。我把这其中的步骤总结为下面几步

1.类加载检查
当虚拟机接受到new指令时,首先去查常量池中能否定位到这个类的符号引用,并且检查这个符号引用的类是否已经被加载、解析和初始化过。如果没有那就必须执行类的加载过程。简单来说,就是虚拟机中有没有这个类的信息,如果没有就得去加载。

2.为对象分配内存
对象需要的内存在类加载完成之后就已经完全确定了,为对象分配内存的任务等同于在Java堆上划分出一块确定大小的内存。

这个划分内存的动作有两种情况。

如果Java堆中的内存时规整的,使用过的放一边,未使用的放另一边,中间用一个指针作为分界点的指示器。那么分配内存的动作就是将指针向未使用的那一边移动所需要的内存大小。这种分配方式叫做指针碰撞(Bump the Pointer)。

如果Java堆中的内存时不规整的,这种情况很明显不适用于指针碰撞,一般这种情况下Java虚拟机会维持一张表来记录内存的使用情况,哪些内存使用了,哪些内存时空闲的都会记录好,需要分配内存时在表中查找到合适的内存区域分配,然后更新这张表即可。这张表被称为空闲列表(Free List)

使用指针碰撞还是空闲列表由Java堆是否规整决定,而Java堆是否规整则由Java虚拟机的GC算法是否带有压缩整理功能决定。

在并发环境下,简单的修改指针指向的内存位置并不安全,因为A对象分配内存的时候,指针还没有移动的同时,B对象也要开始分配内存,因此使用的还是未发生改变的指针。解决方案有两种,一种是对分配内存空间的动作作同步处理————实际上虚拟机采用CAS配上失败重试的方式来保证操作的原子性;另一种是把内存分配动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程分配内存,就在那个线程的TLAB上分配,只有TLAB分配完并分配新的TLAB时,才需要同步锁定。

3.内存初始化
在分配完内存后,虚拟机需要将分配到的内存空间初始化为0(不包括对象头),如果使用TLAB,那么在TLAB分配时就可以完成这一步骤。这一步骤保证了对象中的字段不初始化就能直接使用。

package test;

public class NoInitInt {
    private int noInitInteger;

    public static void main(String[] args){
        int i = 0;
        System.out.println(i);
    }

    public int getNoInitInteger() {
        return noInitInteger;
    }

    public void setNoInitInteger(int noInitInteger) {
        NoInitInt noInitInt = new NoInitInt();
        System.out.println(noInitInt.getNoInitInteger());
    }
}

如上程序所示,输出为0

4.对象设置

Java虚拟机需要设置对象是哪个类的实例,如果才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。这些信息被存放在对象的对象头中(Object Header)

5.初始化

经过上面的步骤,从Java虚拟机的角度一个新的对象已经生成了,但是从Java程序员的角度,这个对象还差一步,就是对象的初始化