cugxchen / cugxchen.github.io Goto Github PK
View Code? Open in Web Editor NEWMy blog website
My blog website
调用start()方法会启动新线程,新线程会自动执行run()方法,run()方法执行完毕,线程生命周期结束。start()方法不能被重复调用,否则抛异常程序终止。
run()方法是线程的执行体,可以与普通方法一样被主动重复调用,单独调用run()方法的话,会在当前线程中执行run(),而不会启动新线程,启动新线程需要start()方法。
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("currentThreadName=" + Thread.currentThread());
}
});
thread.run();//将run()方法当普通方法调用,不会创建新线程
thread.run();//将run()方法当普通方法调用,可以重复调用
thread.start();//创建新线程,并自动调用run()方法
//thread.start();//多次调用start()抛异常
}
执行结果:
Thread的start()方法源码
思考:为何start()方法要加synchronized关键字修饰,不加会有什么问题?
在Java中,每个对象有且仅有一个同步锁,意味着同步锁依赖对象而存在。当线程访问某对象的synchronized区域(synchronized方法或代码块)时,其他线程对该对象的所有synchronized区域的访问将被阻塞(因为方法或代码块所属的对象被锁定)。而对该对象的非同步区域可以正常访问。
synchronized关键字被编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果程序中synchronized明确指定了对象参数(synchronized修饰代码块),那就是这个对象的reference;如果没有明确指定(synchronized修饰的是方法),那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或者类对象作为锁对象。
synchronized实例1
线程锁定相同对象,其他线程对该synchronized区域不能访问
class MyRunable implements Runnable {
public void run() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread() + " loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
}
public class ThreadSynchronized {
public static void main(String[] args) {
Runnable runnable = new MyRunable(); // 新建“Runnable对象”
Thread t1 = new Thread(runnable, "t1"); // 新建“线程t1”, t1是基于demo这个Runnable对象
Thread t2 = new Thread(runnable, "t2"); // 新建“线程t2”, t2是基于demo这个Runnable对象
t1.start(); // 启动“线程t1”
t2.start(); // 启动“线程t2”
}
}
执行结果
先将t1线程执行完,再执行t2线程。
因为synchronized(this)锁定的是MyRunable类的对象实例runnable,而t1与t2两个线程的执行体是同一个runnable对象,因此当t1锁定时,t2并不能访问,直到t1执行完毕释放锁,t2才能获取锁继续执行。
synchronized示例2
线程锁定相同对象,其他线程对该对象的其他synchronized区域也不能访问
class Count {
// 含有synchronized同步块的方法
public void synMethod() {
synchronized(this) {//锁定Count对象实例
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
// 也包含synchronized同步块的方法
public void synMethod2() {
synchronized(this) {//锁定Count对象实例
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " synMethod2 loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
}
public class ThreadSynchronizedTwo {
public static void main(String[] args) {
final Count count = new Count();
// 新建t1, t1会调用“count对象”的synMethod()方法
Thread t1 = new Thread(
new Runnable() {
public void run() {
count.synMethod();
}
}, "t1");
// 新建t2, t2会调用“count对象”的nonSynMethod()方法
Thread t2 = new Thread(
new Runnable() {
public void run() {
count.synMethod2();
}
}, "t2");
t1.start(); // 启动t1
t2.start(); // 启动t2
}
}
执行结果
虽然两个线程访问的代码块不同,但仍是先将t1线程执行完,再执行t2线程。
因为两个线程锁定的是同一个对象count,即使两线程访问不同的synchronized同步代码块,t2仍需要等待t1执行完毕,释放count对象锁之后才能执行。
synchronized示例3
线程锁定相同对象,其他线程对该对象的非synchronized区域可以访问
class MyCount {
// 含有synchronized同步块的方法
public void synMethod() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
// 非同步的方法
public void nonSynMethod() {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
public class ThreadSynchronizedThree {
public static void main(String[] args) {
final MyCount count = new MyCount();
// 新建t1, t1会调用“count对象”的synMethod()方法
Thread t1 = new Thread(
new Runnable() {
public void run() {
count.synMethod();
}
}, "t1");
// 新建t2, t2会调用“count对象”的nonSynMethod()方法
Thread t2 = new Thread(
new Runnable() {
public void run() {
count.nonSynMethod();
}
}, "t2");
t1.start(); // 启动t1
t2.start(); // 启动t2
}
}
执行结果
t1与t2交替执行。
虽然t1保留了count对象锁,但t2执行的是非synchronized同步区域,不需要等待对象锁,所以t1获取对象锁,不影响t2的运行。
synchronized示例4
线程锁定不同对象,对不同对象的synchronized区域可以访问
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
}
public class ThreadSynchronizedFour {
public static void main(String[] args) {
Thread t1 = new MyThread("t1"); // 新建“线程t1”
Thread t2 = new MyThread("t2"); // 新建“线程t2”
t1.start(); // 启动“线程t1”
t2.start(); // 启动“线程t2”
}
}
执行结果
t1与t2交替执行。
因为t1与t2锁定的是不同的MyThread对象实例,因此相互不影响。
实例锁:锁在某个实例对象上,实例锁对应的是synchronized关键字。
全局锁:锁针对的是类,无论该类有多少对象,线程都共享该锁,全局锁对应的是static synchronized关键字(或者是锁在该类的class或者classloader对象上)。
假设代码如下:
pulbic class Something {
public synchronized void syncA(){}
public synchronized void syncB(){}
public static synchronized void staticSyncA(){}
public static synchronized void staticSyncB(){}
}
Something x = new Something();
Something y = new Something();
分析下面4组情况能否同时访问:
(1)x.syncA与x.syncB
(2)x.syncA与y.syncA
(3)x.staticSyncA与y.staticSyncB
(4)x.syncA与Something.staticSyncB
抓住synchronized同步锁依赖对象而存在,只要分析出锁定的对象是否相同,不难分析出:
(1) 不能同时访问,因为它们锁定的对象相同,都是x对象实例;
(2) 可以同时访问,因为它们锁定的对象不同,分别是x对象实例和y对象实例;
(3) 不能同时访问,因为它们锁定的对象相同,都是Something类实例;
(4) 不能同时访问,因为它们锁定的对象不同,分别是x对象实例和Something类实例。
在Object.java中,定义了wait(), notify()和notifyAll()等接口。
wait()的作用是让当前线程(CPU正在运行的线程)进入等待状态,同时,wait()也会让当前线程释放它所持有的锁(而阻塞在sleep()方法时不会释放锁)。
而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程(如有多个线程等待,随机唤醒其中一个线程),而notifyAll()是唤醒所有的线程。
//Wait()使当前线程等待
class TestThread extends Thread{
public TestThread(String name) {
super(name);
}
public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName()+" call notify()");
// 唤醒当前的wait线程
notify();
}
}
}
public class WhichThreadWait {
public static void main(String[] args) {
TestThread t1 = new TestThread("t1");
synchronized(t1) {
try {
// 启动“线程t1”
System.out.println(Thread.currentThread().getName()+" start t1");
t1.start();
// 主线程等待t1通过notify()唤醒。
System.out.println(Thread.currentThread().getName()+" wait()");
t1.wait();//为何这里是主线程等待,而不是t1线程等待?
System.out.println(Thread.currentThread().getName()+" continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果:
t1.wait()为何这里是主线程等待,而不是t1线程等待?
JDK中的解释 “ Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. ”, wait()的作用是让“当前线程”等待,即正在CPU上运行的线程。虽然t1.wait()是通过“线程对象t1”调用的wait()方法,但是执行t1.wait()的地方是在主线程,即主线程是当前线程,需要处于等待状态。
唤醒所有在此监视器上等待的线程
public class WaitAndNotify {
private static Object obj = new Object();
public static void main(String[] args) {
MyThread t1 = new MyThread("t1");
MyThread t2 = new MyThread("t2");
MyThread t3 = new MyThread("t3");
t1.start();
t2.start();
t3.start();
try {
System.out.println(Thread.currentThread().getName()+" sleep(3000)");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj) {
System.out.println(Thread.currentThread().getName()+" notifyAll()");
obj.notifyAll();//唤醒所有wait()线程
//obj.notify();//唤醒单个wait()线程
}
}
static class MyThread extends Thread{
public MyThread(String name){
super(name);
}
public void run() {
synchronized (obj) {
try {
// 打印输出结果
System.out.println(Thread.currentThread().getName() + " wait");
//当前线程进入等待状态,释放锁
obj.wait();
// 打印输出结果
System.out.println(Thread.currentThread().getName() + " continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
执行结果:
主线程通过notifyAll()唤醒所有等待的子线程。
思考: notify()是依据什么唤醒等待线程的,即wait()等待线程与notify()之间通过什么关联起来的?为何notify(),wait()等方法是定义在Object中,而不是Thread中?
wait()与notify()方法的联系依据是对象同步锁。使用wait()与notify()方法必须要有对象锁,否则运行时抛非法监视器的异常。唤醒线程(负责唤醒等待线程的那个线程),只有在获取该对象的同步锁(必须与等待线程是同一个锁),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然等待线程被唤醒,但是它不能立即执行,需要等待唤醒线程释放对象的同步锁之后,等待线程才能获取对象同步锁进而继续执行。
既然notify()与wait()依赖于同步锁(不是依赖线程),而同步锁是对象所持有,并且每个对象有且仅有一个。因此notify()与wait()等方法需要定义在Object()类,而不是Thread类中。
##5、线程同步之join()##
让当前执行join()方法的线程等待,直到等待超时或拥有join()方法的线程执行完毕。
public class JoinTest{
public static void main(String[] args){
try {
ThreadA t1 = new ThreadA("t1"); // 新建“线程t1”
t1.start(); // 启动“线程t1”
t1.join(); //“主线程main()会等待"线程t1"完成”
System.out.printf("%s finish\n", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public void run(){
System.out.printf("%s start\n", this.getName());
// 延时操作
try {
sleep(3000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.printf("%s finish\n", this.getName());
}
}
}
##6、Thread与Runnable的关系##
Thread是线程类,Runnable是接口。如果只使用Thread创建新线程,需要自定义线程类继承Thread,并重写run()方法,因为线程的执行体是run(),如果不自定义线程类,则无法重写run()方法,即不能执行我们需要新线程运行的内容。Runnable接口解决了必须自定义线程类,才能创建满足需求线程的弊端,简化了线程创建流程,并且可以多线程共享同一个target对象(线程执行体,即run()方法体)。
如果既自定义了线程类继承Thread,又使用了Runnable接口,必须在自定义线程类中重写run()方法,并且调用super.run(),否则Runnable接口实例无法执行。
class MyThreadTest extends Thread{
public MyThreadTest(String namString){
super(namString);
}
public MyThreadTest(Runnable runnable, String namString){
super(runnable, namString);
}
@Override
public void run() {
super.run();//需要调用super.run()方法,否则传入的Runnable实例无法执行
System.out.println(Thread.currentThread() + " MyThread run method");
}
}
public class ThreadAndRunnable {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + " Runnable run method");
}
};
//直接创建Thread实例,Runnable创建线程执行体
System.out.println("直接创建Thread实例t1,Runnable创建线程执行体");
Thread thread1 = new Thread(runnable, "t1");
thread1.start();
//自定义类继承Thread,创建线程实例
System.out.println("自定义类继承Thread,创建线程实例t2");
MyThreadTest thread2 = new MyThreadTest("t2");
thread2.start();
//既自定义类继承Thread,又使用了Runnable接口
System.out.println("既自定义类继承Thread,又使用了Runnable接口,创建线程t3");
MyThreadTest thread3 = new MyThreadTest(runnable, "t3");
thread3.start();
}
}
##7、总结##
线程创建后需要调用start()方法才能启动,且只能调用一次,否则抛异常;run()方法是线程的执行体,线程启动后自动调用该方法;run()可以主动调用,但主动调用是在当前线程执行,而不是在新建线程执行。
synchronized是同步锁关键字,每个对象有且仅有一个同步锁,同步锁要依赖对象而存在。Synchronized关键字被编译后,会在同步块的前后分别生成monitorenter和monitorexit这两个字节码指令,这两个字节码需要一个类型参数来指明锁定和解锁的对象。如果程序中明确指定了synchronized对象参数,那么会以对象实例作为锁对象;如果没有明确指定synchronized对象参数,那么会将类对象作为锁对象。
如果锁定的对象相同,则所有synchronized区域均不能多线程访问(非synchronized区域仍可以多线程访问);如果锁定的对象不同,则synchronized区域可以多线程同时访问互不干扰。
wait()、notify()和notifyAll()等接口是在Object类中定义的,wait()的作用是让当前线程(CPU正在运行的线程)进入等待状态,同时,wait()也会让当前线程释放它所持有的锁;notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程。wait()与notify()或notifyAll()配合使用,可以很方便的控制线程同步。wait()与notify()方法是依赖对象同步锁联系起来的,而同步锁是对象所持有,且每个对象有且仅有一个同步锁,因此notify()与wait()等方法需要定义在Object()类,而不是Thread类中。
join()方法是使当前线程处于阻塞状态,等待拥有该join()方法的线程执行完毕或等待超时。join()可以很方便的控制线程执行顺序,保证多线程时序。
当一个对象不再使用时,本该被回收,而另一个正在使用的对象持有它的引用导致不能被回收,就产生了内存泄漏。
Android系统为每个应用程序分配的内存有限,当应用中内存泄漏较多时,轻则造成可用空间不足,频繁发生gc,表现为应用运行卡顿;重则导致内存溢出应用crash。
public class AppManager{
private static volatile AppManager instance = null;
private Context mContext;
private AppManager(Context context){
this. mContext = context;
}
public static AppManager getInstance(Context context){
if(null == instance){
synchronized(AppManager.class){
if(null == instance){
instance = new AppManager(context);
}
}
}
return instance;
}
}
ps:单例模式为何加双重检查?申明单例的静态引用为何加volatile
关键字?
由于单例的生命周期和Application一样长,当Context所对应的Activity退出时,由于单例持有该Activity的引用,造成Activity退出时内存不能得到回收。
修正方法:
this. mContext = context;
改为
this. mContext = context.getApplicationContext();
无论传入什么Context,最终都将使用Application的Context,防止了内存泄漏。
public class OuterClassActivity extern Activity{
private static Inner mInner = null;
@Override
protected void onCreate (Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(null == mInner){
mInner = new Inner();//创建静态实例
}
}
private class Inner{//非静态内部类
//TODO
}
}
由于非静态内部类会持有外部类的引用,当在内部类创建非静态内部类的静态实例后,导致该静态实例会持续持有外部类的应用,造成内存资源不能正常回收。
修正方法:将内部类设为静态内部类,静态内部类不会持有外部类实例的引用,当需要用到外部类的方法或属性时,使用外部类实例的弱引用。例如内部类可以这么写
private static class Inner{
private WeakReference<OuterClassActivity> mActivity;
public Inner(OuterClassActivity activity){
mActivity = new WeakReference<OuterClassActivity>( activity);
}
}
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//TODO
}
};
}
由于mHandler是Handler的非静态匿名内部类的实例,它会持有外部类Activity的引用。消息队列是在一个Looper线程中不断轮询处理消息,如果当这个Activity退出时消息还未处理完毕,消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,导致Activity退出时无法释放内存,引发内存泄漏。
修正方法:创建静态的Handler内部类,然后对Handler持有的activity对象使用弱引用,这样可以避免Activity内存泄漏,不过Looper线程的消息队列还是可能会有待处理的消息,所以在Activity 的onDestory方法中应该移除消息队列中的消息。
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new MyHandler (this);
private static class MyHandler extern Handler{
private WeakReference<Context> softRefContext;
public MyHandler(Context context) {
softRefContext = new WeakReference<Context>(context);
}
@Override
public void handleMessage(Message msg) {
//TODO
}
};
@Override
protected void onDestroy() {
if (mHandler != null){
mHandler.removeCallbacksAndMessages(null);
mHandler = null;
}
super.onDestroy();
}
}
为避免阻塞主线程,在Activity中创建线程执行耗时操作是比较常见的,如
new Thread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "线程创建成功,正在执行线程程序");
SystemClock.sleep(30*1000);
//TODO
}
}).start();
上面的Runnable是一个匿名内部类,因此它对当前Activity有一个隐式引用。如果Activity在销毁之前,任务还未完成,将导致Activity的内存资源无法回收,造成内存泄漏。
正确做法:使用静态内部类,
static class MyRunnable implements Runnable{
@Override
public void run() {
Log.i(TAG, "线程创建成功,正在执行线程程序");
SystemClock.sleep(30*1000);
}
}
//--------------------------------------------------------------------------
new Thread(new MyRunnable()).start();
在Activity销毁时应该取消相应的任务,避免在后台执行浪费资源。
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap
等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
非静态内部类、匿名内部类会隐式持有外部类对象,需要注意其生命周期,建议使用静态内部类配合弱引用访问外部类,避免一不小心造成内存泄漏。
并不是所有内部类只能使用静态内部类,当该类的生命周期不可控时,我们需要采用静态内部类。
内部类存在的意义:接口只能解决部分多重继承问题,而内部类可以使多重继承更加完善,当需要继承抽象类或者具体类时,只能使用内部类才能实现多重继承。
内存泄漏主要分为以下几种类型:
1.静态变量(包括但不限于单例)引起的内存泄漏。注意静态变量持有对象的生命周期。
2.非静态内部类引起的内存泄漏。静态内部类,弱引用访问。
3.匿名内部类引起的内存泄漏。静态内部类,弱引用访问。
4.资源未关闭引起的内存泄漏。退出前关闭资源。
package com.test.aidltestservice;//包名,注意ADIL文件在工程中的存放位置要与包名一致
// Declare any non-default types here with import statements
interface IMyAidlInterface {//接口名
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
String myAidlTest();//提供进程间调用的方法
}
注:服务端和客户端都需要该文件,并且文件名和存放路径必须相同
创建类继承Service
public class AIDLRemoteService extends Service {
public final String TAG = this.getClass().getSimpleName();
/*
* Service生命周期方法
*/
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "onCreate");
}
@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
super.onDestroy();
}
/*
* 创建AIDL服务Binder
*/
private IMyAidlInterface.Stub mBinder = new IMyAidlInterface.Stub(){
@Override
public String myAidlTest() throws RemoteException {//ADIL文件中定义的方法名
return "Hello AIDL test";
}
};
/*
* Binder方法重载
*/
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "onBind");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
Log.i(TAG, "onUnbind");
return super.onUnbind(intent);
}
}
<service android:name="com.coship.aidltestservice.AIDLRemoteService">
<intent-filter>
<action android:name="aidltestservice.AIDLRemoteService"/>
</intent-filter>
</service>
注:action android:name 才是客户端绑定时初始化话Intent的名称,而不是service name
public class ClientMainActivity extends Activity{
public final String TAG = this.getClass().getSimpleName();
private IMyAidlInterface mConnectIMyAidlInterface;//通过该实例调用AIDL方法
private final static AIDL_INTENT_NAME= "aidltestservice.AIDLRemoteService";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG,"onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client_main);
mContext = this;
bindAidlService();
}
@Override
protected void onResume() {
Log.i(TAG,"onResume");
try{
String string = mConnectIMyAidlInterface.myAidlTest();//调用AIDL接口,返回的是"Hello AIDL test"
}
catch (RemoteException e){
Log.w(TAG, "服务调用失败");
}
}
private void bindAidlService(){
Intent intent = new Intent(AIDL_INTENT_NAME);
boolean ret = bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
Log.i(TAG, "创建AIDL链接是否成功:"+ret);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder){
mConnectIMyAidlInterface = IMyAidlInterface.Stub.asInterface(binder);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mConnectIMyAidlInterface = null;
}
};
}
注意事项:
1、客户端与服务端都要包含同一套AIDL文件,并且路径相同;
2、服务端AndroidManiest.xml必须定义service和,否则绑定不成功;
3、客户端绑定Binder的名称为service的action android:name而不是AIDL的包名或者service name
1、哪些内存需要回收?
2、什么时候回收?
3、如何回收?
下面是Java虚拟机运行时数据区示意图:
Java内存运行时区域的各个部分,程序计数器、虚拟机栈、本地方法栈3个区域随线程生灭;栈中的栈帧随方法的进入和退出有条不紊的进栈出栈。这几个区域的内存分配和回收具备确定性,不需要过多考虑回收问题。
而Java堆和方法区(永久代),只有在程序运行期才能知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集器需要关注的是这部分内存。其中永久代的回收条件非常苛刻,在方法区进行垃圾回收“性价比”比较低,Java虚拟机规范明确说明可以不要求虚拟机在方法区实现垃圾收集。我们讨论的垃圾收集是在Java堆的内存回收。
给对象添加一个引用计数器,对象被引用时,计数器加1;当引用失效时,引用计数器减1;任何时刻计数器为0的对象就代表不再被使用的“死”对象,需要被回收。
引用计数算法实现简单,判定高效,C++智能指针、Objective-C的ARC机制等内存管理是使用这种算法管理内存。但是这种算法有个很大的缺点是循环引用问题,例如:objA与objB两个对象,已经不被任何其他地方使用,本应被回收;然而这两个对象相互持有,导致计数不为0,收集器无法回收它们。
循环引用问题,需要程序员时刻注意,在出现循环引用的地方,其中一个对象使用弱引用不让引用计数加1。弱引用可以解决该问题,但是需要程序员时刻注意。因此Java虚拟机没有选用引用计数算法进行内存管理。
主流的商业程序语言(Java、C#)都是通过可达性分析来判断对象是否存活的。这种算法的基本**是通过一系列“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。整个可达性分析期间,整个执行系统需要”冻结”在某个时间点,避免出现分析过程中对象的引用关系还在不断变化的情况,如果不满足的话发分析结果的准确性无法得到保证,这是导致GC进行时必须停顿所有Java线程的重要原因。
Java语言中,可以作为GC Roots的对象包括下面几种:
(1)虚拟机栈的本地变量表中引用的对象;
(2)方法区中类静态属性引用的对象;
(3)方法区中常量引用的对象;
(4)本地方法栈中JNI引用的对象。
即使在可达性分析算法中不可达的对象,也不是立即回收,这时候还处于“缓刑”阶段,要真正宣告对象死亡,至少要经历两次标记。
如果对象没有到GC Roots的引用链,它会被进行一次标记筛选,如果对象有覆盖finalize()方法,并且没有执行过该方法,那么这个对象会被放置到一个叫做F-Queue的队列中,并且由虚拟机自建的低优先级finalizer线程去执行它finalize()
。finalize()方法是对象逃脱死亡命运的最后一次机会,GC会将F-Queue队列中的对象进行第二次标记,如果对象要在finalize()方法中拯救自己,只需要重新与引用链建立连接,如果对象这时候还没有逃脱,就真的被回收了。
/*
* 演示 对象在finalize()方法中自救(不建议这么使用)
*/
public class FinalizeEscapeGC{
public static FinalizeEscapeGC saveMyself = null;
public void isAlive() {
System.out.println("我还活在!");
}
@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("finalize() 方法被执行,我来拯救自己!");
FinalizeEscapeGC.saveMyself = FinalizeEscapeGC.this;
}
public static void main(String[] args) throws InterruptedException {
FinalizeEscapeGC.saveMyself = new FinalizeEscapeGC();
//对象第一次拯救自己,拯救成功
FinalizeEscapeGC.saveMyself = null;
System.gc();
Thread.sleep(100);
System.out.println("第一次成功拯救自己");
if (FinalizeEscapeGC.saveMyself != null) {
FinalizeEscapeGC.saveMyself.isAlive();
}else {
System.out.println("FinalizeEscapeGC 对象已死");
}
//对象第二次拯救自己,拯救失败
FinalizeEscapeGC.saveMyself = null;
System.gc();
Thread.sleep(100);
System.out.println("第二次拯救自己失败");
if (FinalizeEscapeGC.saveMyself != null) {
FinalizeEscapeGC.saveMyself.isAlive();
}else {
System.out.println("FinalizeEscapeGC 对象已死");
}
}
}
两段一样的代码,执行结果是第一次逃脱成功,第二次失败,因为任何一个对象的finalize()方法都只会被执行一次。finalize()自救的方法不建议使用,因为它运行代价高,不确定性大,无法保证各个对象的调用顺序。
该算法分为“标记”、“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。
存在问题:1、效率问题,标记与清除的效率都不高(地址空间分散,需要执行多次清理操作);2、空间问题,标记清除后存在大量不连续的内存碎片。
将内存按容量均分为两块,每次只使用其中一块。当一块内存用完,就将还存活的对象复制到另一块,然后把使用过的内存块一次清理掉。这样每次都是半块内存回收,内存分配时就不用考虑内存碎片的问题,实现简单,运行高效,但是内存利用率不高,按1:1分配时只有50%的内存被有效使用,适用于每次整理只有少量对象存活的情况。
标记整理算法与标记清理算法类似,但后续不是直接对可回收对象清理,而是让所有存活对象向一端移动,然后清理端边界以外的内存。
从内存回收的角度来看,收集器基本都采用分代收集算法。Java堆可细分为:新生代和老年代。新生代每次垃圾收集都发现大批对象死去,只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成复制。老年代中因为对象存活率高,使用复制算法效率会很低,而且没有额外空间对它进行分配担保,就必须使用标记清理算法或者标记整理算法进行回收。
新生代中的对象绝大部分“朝生夕死”,使用复制算法时并不需要1:1划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间(比例8:1:1)。这样划分的目的是更好地回收内存,或者更快地分配内存。
每次使用Eden和其中一块Survivor。当回收内存时,将Eden和Survivor中还存活的对象一次性复制到另一个Survivor上,最后清理掉Eden和刚才用过的Survivor空间。当Survivor空间不足时,需要依赖老年代进行分配担保。如果另一块Survivor空间没有足够的空间存放上一次新生代存活的对象,这些对象将直接通过分配担保机制进入老年代。
新对象优先在Eden区分配,当Eden区空间不足时,将触发一次Minor GC(新生代GC)。
大对象直接进入老年代,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
长期存活的对象将进入老年代,对象在Survivor每熬过一次MinorGC,年龄+1,当年龄增加到一定程度(默认15),就会晋升到老年代中。
老年代GC(MajorGC/Full GC),出现Full GC通常至少会伴随一次MinorGC,MajorGC的速度一般会比MinorGC慢10倍以上。
Java内存回收主要关注Java堆,为了内存管理的方便,一般将Java堆分为新生代、老年代。
新生代的特点是大部分对象“朝生夕死”存活率低,适合使用复制算法。将新生代分为一个较大的Eden区和两个较小的Survivor区,分配比例一般为8:1:1,这样新生代最大可用内存为新生代容量的90%,我们没有办法保证每次回收都只有少于10%的对象存活,当Survivor区空间不够保留存活对象时,依赖其他内存(老年代)进行分配担保。长期存活的新生代对象,达到一定“年龄”后会进入老年代中。
老年代中的对象存活率高,复制算法需要进行较多的复制操作,效率将会变低,而且需要分配担保,因此复制算法不适用与老年代,而是选用标记整理算法。老年代GC速度一般比新生代慢10倍以上,但是老年代一般不会频繁回收内存。
我们进行Java编程时,尽量不要分配短命的大对象,这样会频繁触发老年代GC,严重影响运行效率。能使用普通类型,不要使用类类型,例如表示整数,尽可能使用int类型,而不是Integer类。Java堆也不是越大越好,Java堆越大,GC的频繁降低,但是单次GC的耗时大大增加,影响用户体验。
先来看两段段简短的代码:
示例1:
public String nullStringTest(){
String s = null;
s += "abc";
return s;
}
示例2:
public void stringPool(){
String a="hello";
String b="hell";
String c=b+"o";
String d="hell"+"o";
System.out.println(a == b+new String("o"));
System.out.println(a == c);
System.out.println(a == d);
}
打印结果各是什么?
示例1的反编译结果如下:
注:java的反编译指令 javap -verbose 类名.class
0: aconst_null
1: astore_1
2: new #5 // class java/lang/StringBuilder
5: dup
6: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
9: aload_1
10: invokevirtual #7 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;
13: ldc #8 // String abc
15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: invokevirtual #9 // Method java/lang/StringBuilder.toString: ()Ljava/lang/String;
21: astore_1
22: aload_1
23: areturn
由反编译代码可以看出,代码
s += "abc";
等价于
StringBuilder tmp = new StringBuilder();
tmp.append(String.valueOf((Object)null));
tmp.append("abc");
s = tmp.toString();
而String
的valueOf(Object ojb)
方法实现源码如下:
因此示例1的结果为nullabc
示例2的反编译代码比较长,就不贴出了,有兴趣的朋友自行反编译出来研读。在分析示例2的结果之前,先来看一个更简单的示例3:
String str1 = "abc";
String str2 = new String("abcd");
示例3的内存分配情况如下图:
String str1 = "abc";
先有字符串"abc"
存在于常量池,然后Java栈上的str1
执行常量池字符串"abc"
;
String str2 = new String("abcd");
先有字符串"abcd"
放入常量池,然后new
了一份字符串"abcd"
放入Java堆(字符串常量"abcd"
在编译期就已经确定放入常量池,而Java堆上的"abcd"
是在运行期初始化阶段才确定,因此先有常量池"abcd"
,再有Java堆”abcd”),然后Java栈的str2
指向Java堆的"abcd"
。
如果示例3的内存分配可以理解,就不难得出示例2的内存分配情况:
示例2的执行结果为:
思考:String
类为何设计为不可变?
String是字符串常量的引用,String += String
的本质是new
了新的临时对象StringBuild
,拼接后再StringBuild.toString
赋给原String
。所有大量字符串拼接不要直接使用String
,否则会生成大量临时对象,严重影响性能。
StringBuild进行字符串拼接不会生成临时对象,效率高,但不是线程安全的。
StringBuffer进行字符串拼接也不会生成临时对象,效率略低于StringBuild
,线程安全。
示例4是以上三种类进行大量字符串拼接的示例代码:
private static int LOOP_TIMES = 10000;
private Random ran = new Random();
public void loopString(){
String string = "";
for (int i=0; i<LOOP_TIMES; i++){
string += ran.nextInt();
}
}
public void loopStringBuild(){
StringBuilder stringBuilder = new StringBuilder();
for (int i=0; i<LOOP_TIMES; i++){
stringBuilder.append(ran.nextInt());
}
}
public void loopStringBuffer(){
StringBuffer stringBuffer = new StringBuffer();
for (int i=0; i<LOOP_TIMES; i++){
stringBuffer.append(ran.nextInt());
}
}
执行结果:
I/MainActivity: loopString -->18435ms
I/MainActivity: loopStringBuild -->10ms
I/MainActivity: loopStringBuffer -->10ms
String的+操作是一种语法糖,其本质是创建了临时的StringBuild对象进行append操作,然后toString()赋给原来的String引用,因此大量字符串拼接不要直接用String,应该使用StringBuild或StringBuffer,其中StringBuild不考虑线程同步,效率更高,StringBuffer考虑线程安全,效率略低于StringBuild。
Android打印调试类,使用的时候将Log换成LogTools即可,其他地方不变。
public class LogTools {
private static final String DEBUG_TAG = "DEBUG";//打印统一使用这个,方便跟踪打印调试
private static boolean isUniPrint = "true".equals(Tools.getProperties("persist.sys.uniprint", "false")) ? true : false;//是否使用统一的打印TAG
public static void v(String tag, String msg){
if (isUniPrint){
Log.v(DEBUG_TAG, LogTools.appendExtraStr(msg));
}else {
Log.v(tag, LogTools.appendExtraStr(msg));
}
}
public static void i(String tag, String msg){
if (isUniPrint){
Log.i(DEBUG_TAG, LogTools.appendExtraStr(msg));
}else {
Log.i(tag, LogTools.appendExtraStr(msg));
}
}
public static void d(String tag, String msg){
if (isUniPrint){
Log.d(DEBUG_TAG, LogTools.appendExtraStr(msg));
}else {
Log.d(tag, LogTools.appendExtraStr(msg));
}
}
public static void w(String tag, String msg){
if (isUniPrint){
Log.w(DEBUG_TAG, LogTools.appendExtraStr(msg));
}else {
Log.w(tag, LogTools.appendExtraStr(msg));
}
}
public static void e(String tag, String msg){
if (isUniPrint){
Log.e(DEBUG_TAG, LogTools.appendExtraStr(msg));
}else {
Log.e(tag, LogTools.appendExtraStr(msg));
}
}
private static String appendExtraStr(String msg){
StringBuilder stringBuilder = new StringBuilder(128);
stringBuilder.append(msg).append(" [").append(LogTools._FUNC_()).append("; ").append(LogTools._FILE_()).append(":").append(LogTools._LINE_()).append("]");
return stringBuilder.toString();
}
// 当前文件名
private static String _FILE_() {
StackTraceElement traceElement = ((new Exception()).getStackTrace())[3];
return traceElement.getFileName();
}
// 当前方法名
private static String _FUNC_() {
StackTraceElement traceElement = ((new Exception()).getStackTrace())[3];
return traceElement.getMethodName();
}
// 当前行号
private static int _LINE_() {
StackTraceElement traceElement = ((new Exception()).getStackTrace())[3];
return traceElement.getLineNumber();
}
//打印当前调用堆栈
public static void printCallStatck() {
Throwable ex = new Throwable();
StackTraceElement[] stackElements = ex.getStackTrace();
StringBuilder logoutBuilder = new StringBuilder();
if (stackElements != null) {
logoutBuilder.append("---------------------------------------------");
for (int i = 0; i < stackElements.length; i++) {
logoutBuilder.append(stackElements[i].getClassName()+"/");
logoutBuilder.append(stackElements[i].getFileName()+"/");
logoutBuilder.append(stackElements[i].getLineNumber()+"/");
logoutBuilder.append(stackElements[i].getMethodName()+"\r\n");
}
logoutBuilder.append("---------------------------------------------");
}
LogTools.d(DEBUG_TAG, logoutBuilder.toString());
}
}
在java语言中类的加载、连接和初始化过程都是在程序运行期间完成的,因此在类加载时的效率相对编译型语言较低。除此之外,任何一个类只有在运行期间使用到该类的时候才会将该类加到内存中。总之,java依赖于运行期间动态加载和动态链接来实现类的动态使用。其整个流程如下:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后,这是为了支持Java语言的动态绑定。
什么情况下开始类加载,Java虚拟机规范中没有强制约束,交给虚拟机具体实现自由把握。但是初始化阶段,虚拟机规范则严格规定了有且仅有5种情况对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
(1) 遇到new(实例化对象)、getstatic(读取静态字段)、putstatic(设置静态字段)、invokestatic(调用静态方法)这4个字节码指令时,如果类没有进行过初始化,则先触发类的初始化。(注:类初始化只会触发一次,而对象初始化可以多次,例如:new了多个String对象,整个虚拟机进程只会触发一次String类的初始化,但是会触发多次String对象初始化)
(2) 对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。
(3) 初始化一个类的时候,如果发现其父类还没有初始化,则先触发父类初始化,再触发自己类的初始化。
(4) 虚拟机启动时,虚拟机会先初始化主类(包含main()方法的类)
(5) 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果包含REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
以上5种情形会触发类进行初始化,这5种场景称为对一个类进行主动引导;除此之外,多有引用类的方式都不会触发初始化,称为被动引导。以下是3个被动引导的例子:
public class ParentClass {
static{
System.out.println("ParentClass init");
}
public static int value = 123;
public ParentClass() {
System.out.println("new ParentClass instance");
}
}
public class SubClass extends ParentClass{
static{
System.out.println("SubClass init");
}
public SubClass() {
System.out.println("new SubClass instance");
}
}
public class ConstClass {
static{
System.out.println("ConstClass init");
}
public static final String HELLO_WORLD = "hello world";
}
//为防止前面运行的代码影响后面代码的执行结果,模块1、2、3分别运行
public static void main(String[] args) {
/* 模块1
* 输出:ParentClass init
123
* 对于静态字段,只有直接定义这个字段的类才会初始化,因此子类引用父类定义的静态字段,只会触发父类的初始化,而子类不会初始化
*/
System.out.println(SubClass.value);
System.out.println("-------------------------------------");
/* 模块2
* 输出:-------------------------------------
ParentClass init
* 通过数组定义来引用类,不会触发此类的初始化.(数组类本身不是通过类加载器创建,它是由Java虚拟机直接创建)
*/
ParentClass[] parentClasses = new ParentClass[10];
System.out.println("-------------------------------------");
ParentClass objClass = parentClasses[0];
objClass.value = 555;//调用数组类的静态字段,触发类的初始化
/* 模块3
* 输出:hello world
*
* 常量在编译时就放入常量池,本质上没有直接引用定义常量的类,不会触发定义常量的类的初始化
*/
System.out.println(ConstClass.HELLO_WORLD);
}
在加载阶段,虚拟机需要完成以下3件事:
(1)通过类的全局限定名来获取定义此类的二进制字节流;
(2)将字节流转为运行时数据结构;
(3)在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就 Java 虚拟机中的唯一性,也就是说,即使两个类来源于同一个 Class 文件,只要加载它们的类加载器不同,那这两个类就必定不相等。
因为class文件来源不一定是Java源码编译,甚至可能是文本编辑器直接编写来产生Class文件。虚拟机需要检查输入字节流是否合法,是对自身的保护机制。验证阶段的4个校验动作:
(1)文件格式验证,class文件是否符合格式规范,如以0XCAFEBABE开头等等;
(2)元数据验证(数据类型校验),对字节码描述进行语义分析,如:是否有父类,是否继承或覆盖final等等;
(3)字节码验证(方法体校验),例如操作数栈是int类型,却按long加载到本地变量表;不会跳转到方法体以外的字节码指令上;不会将对象付给不相干类数据类型等等。
(4)符号引用验证,如能否通过全限定名找到对应的类,private等访问性是否可被当前类访问。
对于虚拟机的类加载机制来说,验证阶段非常重要,但不是一定是必要的阶段(因为对运行期没有影响),如果所运行的全部代码都已经反复使用验证过,可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类的加载时间。
正式为类变量(没有final的static修饰的变量)分配内存并设置类变量初始值(数据类型的0值)的阶段。例如:public static int value = 123;准备阶段后的初始值是0,而不是123。因为此时尚未执行任何Java方法,赋值为123的指令存放在类构造器()方法中,需要到初始化阶段才会执行。但如果上面的static变量被final修饰,在编译时就会为value生成ConstantValue属性,在准备阶段就会根据ConstantValue设置为123。
与C之类的纯编译型语言不同,Java类文件在编译过程中只会生成class文件,并不会进行连接操作,这意味在编译阶段Java类并不知道引用类的实际地址,因此只能用“符号引用”来代表引用类。在解析阶段,JVM可以通过解析该符号引用,来确定类的真实内存地址。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。包括以下四个部分:
(1)类或接口解析;(2)字段解析;(3)类方法解析;(4)接口方法解析。
类初始化阶段是类加载过程的最后一步,前面由虚拟机主导和控制,初始化阶段才真正执行类中定义的Java程序代码。在准备阶段,变量已经赋值过一次,初始化阶段会根据主观计划初始化变量和其他资源。
初始化阶段是执行类构造器()方法的过程,() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句产生。如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器不会为这个类生成()方法。虚拟机会保证一个类的() 方法在多线程环境中被正确地加锁、同步。
/*
* 演示 类的<clinit>()方法在多线程环境被自动加锁、同步
* 输出: Thread[main,5,main] init class 5秒之后才会有下面的打印
Thread[thread1,5,main]---start
Thread[thread2,5,main]---start
Thread[thread2,5,main] 实例构造方法
Thread[thread2,5,main]---run over!
Thread[thread1,5,main] 实例构造方法
Thread[thread1,5,main]---run over!
*/
public class ClassLoad {
static{
if (true) {
System.out.println(Thread.currentThread() +" init class");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public ClassLoad() {
System.out.println(Thread.currentThread() +" 实例构造方法");
}
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "---start");
ClassLoad classLoad = new ClassLoad();
System.out.println(Thread.currentThread() + "---run over!");
}
};
Thread thread1 = new Thread(runnable, "thread1");
Thread thread2 = new Thread(runnable, "thread2");
thread1.start();
thread2.start();
}
}
从Java虚拟机的角度来看,只存在两种不同的类加载器:
(1) 启动类加载器,由C++实现,是虚拟机的一部分;
(2) 其他类加载器,Java语言实现,独立于虚拟机外部,并且自身继承与抽象类java.lang.ClassLoader
从Java开发人员角度,分为3种系统提供的类加载器:
(1) 启动类加载器,负责将存放在$JAVA_HOME/jre/lib
目录,并且被虚拟机识别的类库加载到虚拟机内存中。
(2) 扩展类加载器,负责加载$JAVA_HOME/jre /lib/ext
中的所有类库,开发者可以直接使用扩展类加载器。
(3) 应用程序类加载器,由于这个类加载器是ClassLoader中getSystemClassLoader()
方法的返回值,所有一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果没有自定义类加载器,一般程序中默认使用的就是这个类加载器。
Java应用程序都是由这3种类加载器相互配合加载,如有必要,还可以加入自定义类加载器。这些类加载器的关系如图所示:
除了顶层的启动类加载器外,其余的类加载器都有自己的父类加载器。
双亲委派模型的工作过程如下:
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。如果父加载器为空了,就代表使用启动类加载器(C++实现)作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib
里未查找到该class),会使用拓展类加载器来尝试加载,继续失败则会使用AppClassLoader来加载,继续失败则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
双亲委派模型的实现代码,摘自android-15\java\lang\ClassLoader.java抽象类
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//首先检查请求的类是否已经被加载过,已加载直接返回
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {//未加载过
try {
clazz = parent.loadClass(className, false);//委派父类加载器加载
} catch (ClassNotFoundException e) {
// Don't want to see this.
}
if (clazz == null) {//父类加载器无法加载,自身findClass方法进行类加载
clazz = findClass(className);
}
}
return clazz;
}
双亲委派模型的好处:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello world");
}
}
这段代码的大致经过:
1.首先寻找jre目录的jvm.dll,并初始化JVM,之后会生成一个启动类加载器(Bootstrap ClassLoader),
2.启动类加载器会加载指定目录下的java核心API(如:jdk1.7.0_17\jre\lib\rt.jar
),并生成扩展类加载器(Extended ClassLoader)实例;
3.扩展类加载器加载指定路径下的扩展java API(如:jdk1.7.0_17\jre\lib\ext\*.jar
),并将父加载器设为启动类加载器(注:是父加载器,而不是父类;启动类加载器的父加载器为null);
4. 启动类加载器生成应用类加载器(AppClass ClassLoader)实例,并将其父加载器设为扩展类加载器;
5. 最后应用类加载器加载ClassPath目录定义的类HelloWorld(工程目录/bin/HelloWorld.class)。
(1)从ClassLoader.java的源码看出,调用loadClass时会先根据委派模型在父加载器中加载,如果加载失败,则会调用当前加载器的findClass来完成加载。
(2)因此我们自定义的类加载器只需要继承ClassLoader,并覆盖findClass方法,下面是一个实际例子,在该例中我们用自定义的类加载器去加载我们事先准备好的class文件。
生成字节码文件MyClass.class,放到F盘根目录(任意目录都行,调用自定义ClassLoader时传入正确路径即可),以备自定义类加载器加载。
public class MyClass {
private String nameString;
public String toString() {
return "我叫 " + "MyClass" + ", 我是由 " + getClass().getClassLoader().getClass()+" 加载进来";
}
}
自定义一个类加载器,需要继承ClassLoader类,并实现findClass方法。
public class MyClassLoader extends ClassLoader{
private String classPathString;
private ClassLoader parentClassLoader;
public MyClassLoader(String path) {
this(path, getSystemClassLoader());
}
public MyClassLoader(String path, ClassLoader parClassLoader) {
super(parClassLoader);
this.classPathString = path;
this.parentClassLoader = parClassLoader;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classNameString = name.substring(name.lastIndexOf(".")+1);//不包含最后一个“.”所有+1
String filePathString = classPathString + classNameString + ".class";
File file = new File(filePathString);
System.out.println("MyClassLoader.findClass() package=" + name + " filePathString=" + filePathString);
try {
byte[] bytes = getClassBytes(file);
//把二进制字节流组成的文件转换为一个java.lang.Class
Class<?> clazz = this.defineClass(name, bytes, 0, bytes.length);
return clazz;
} catch (Exception e) {
// TODO: handle exception
}
return super.findClass(name);
}
/*
* 重载loadClass方法可能会破坏双亲委派机制,不建议重载该方法,除非特殊需求
*
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(name);//首先检查是否被加载过
if (clazz == null) {//未加载过,
try {
if (parentClassLoader != null) {
clazz = parentClassLoader.loadClass(name);//委派父类加载加载
}
} catch (ClassNotFoundException e) {
// TODO: handle exception
//e.printStackTrace();
}
if (clazz == null) {//父加载器无法加载,自身加载
clazz = findClass(name);
}
}
/*
* 如果直接用自己加载类,会提示无法"java.lang.NoClassDefFoundError: java/lang/Object",这个类很早就应该加载过了,为何会抛异常找不到该类呢
* 由此说明:1、加载子类会先检查父类是否加载;
* 2、类的唯一性是由类加载器和类本身共同决定的,同一个类只要加载器不同,就是不同的两个类
*/
// if (clazz == null) {
// clazz = findClass(name);
// }
return clazz;
}
//将.class文件读取为字节流
private byte[] getClassBytes(File file) throws Exception {
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel fileChannel = fileInputStream.getChannel();
ByteArrayOutputStream baosArrayOutputStream = new ByteArrayOutputStream();
WritableByteChannel writableByteChannel = Channels.newChannel(baosArrayOutputStream);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int i;
while (true) {
i = fileChannel.read(byteBuffer);
if (i==0 || i==-1) {
break;
}
byteBuffer.flip();
writableByteChannel.write(byteBuffer);
byteBuffer.clear();
}
fileInputStream.close();
return baosArrayOutputStream.toByteArray();
}
}
public class Main {
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("F:/");
Class<?> clazz = classLoader.loadClass("ClassLoader.MyClass");//带上包名
Object obj = clazz.newInstance();
System.out.println(obj);
}
}
执行结果:
由于loadClass方法的双亲委托机制,需要删除工程中的MyClass.class文件,才会查找指定目录(F:/ MyClass.class)的字节码文件。
哪些场景会用到自定义类加载器呢?
例如:字节码(*.class文件)不在本地工程,而在数据库或云端,进行动态部署;防反编译,java代码很容易反编译,把自己的字节码进行加密(最简单的对字节码进行与0xff异或运算后存储),类加密后就不能再用系统自带的ClassLoader去加载类了(因为加密后,.class不符合标注java字节码格式规范),需要自定义ClassLoader在加载类时先解密类(同样进行一次与0xff异或运算就还原了),再加载。
Java系统提供了3种类加载器,分别是:
启动类加载器,负责加载$JAVA_HOME/jre/lib
目录中能被虚拟机识别的类库,开发者不能直接使用(启动类加载器是C++实现);
扩展类加载器,负责加载$JAVA_HOME/jre /lib/ext
中的所有类库,开发者可以直接使用扩展类加载器(Java实现)。
应用程序类加载器(也称系统类加载器),负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器(Java实现)。
从ClassLoader.java的源码看出,调用loadClass时会先根据委派模型在父加载器中加载,如果加载失败,则会调用当前加载器的findClass来完成加载。因此我们自定义的类加载器只需要继承ClassLoader,并覆写findClass方法。
Java应用程序都是由这3种类加载器相互配合加载,如有必要,还可以加入自定义类加载器。类加载器之间满足双亲委派的关系,即加载类时都是先委派给父加载器加载,如果父加载器不能加载,才由自身加载,如果自己也不能加载,会抛出ClassNotFoundException的异常。双亲委派模型的好处是避免用户自己编写的类动态替换 Java的一些核心类,而且能避免类的重复加载。
参考博客链接 http://blog.csdn.net/seu_calvin/article/details/52315125
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.