Qt first&last note

起因

为了验证底层库的api是否可用需要在各个平台写DemoApp,但是如果每个平台各写一个有点重复工作了,因此需要用Qt写跨平台的应用,所以这可能是第一篇也是最后一篇和Qt相关的note。

概述

Qt是一种跨平台的应用框架和工具集,常见的Android、ios、windows和mac都可以支持。也就是可以一次编码多次编译生成各个不同的可执行文件,当然肯定是有一些约束或者未兼容好的地方。

Qt业务逻辑用的C++,界面用的是QML(Qt Modeling Language),有点类似于JSON和CSS,并且支持JavaScript。

QML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Button {
id: dialButton
objectName: "dialButtonObject"
signal dial(string address)
onClicked: dialButton.dial(dialNumberTextEdit.text);
x: 23
y: 26
text: qsTr("Dial")
}

TextEdit {
id: dialNumberTextEdit
x: 141
y: 36
width: 80
height: 20
font.pixelSize: 12
text: qsTr("")
}

Button {
id: answerButton
objectName: "answerButtonObject"
signal answer()
onClicked: answerButton.answer();
x: 23
y: 128
text: qsTr("Answer")
}

在QML中大致是用如上格式来设计,当然Qt也支持可视化的控件拖动工具。

  • id:可以理解在qml中一个控件的句柄,例如我们希望获得TextEdit的值,那就可以用dialNumberTextEdit.text
  • objectName:在C++中找到该控件的一个类似于id的东西。
  • signal:定义一个信号,可用它的id来发送
    1
    dialButton.dial(dialNumberTextEdit.text);

交互

Qt中很重要的概念就是信号(signal)和槽(slot)。信号是发送方,槽是接收方,由于信号和槽才使得前后端交互。假设我们在界面上有个button,在c++需要监听这个button的点击事件。

1.在QML中控件的onClicked事件发送一个信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Button {
id: dialButton
objectName: "dialButtonObject"
signal dial(string address)
//发送信号dial,参数为dialNumberTextEdit的text
onClicked: dialButton.dial(dialNumberTextEdit.text);
x: 23
y: 26
text: qsTr("Dial")
}
```
1. 新建一个类集成QObject,并定义一个slot,名字不用和信号一样。
```c++
class DialButtonSlot:public QObject {
Q_OBJECT
public slots:
void dial(QString message) {
qDebug() << message;
}
public:
DialButtonSlot();
};

3.将信号和槽连接起来

1
2
3
4
5
6
7
8
9
10
11
//使用component加载qml文件
QQmlComponent component(&engine,QUrl(QStringLiteral("qrc:/main.qml")));
//实例化ViewLoader
QObject *viewLoader = component.create();
//从ViewLoader中找到objectName为"dialButtonObject"的QObject
QObject *dialButton = viewLoader->findChild<QObject*>("dialButtonObject");
//实例化一个Slot
DialButtonSlot dialButtonSlot;
//sender:dialButton
//receiver:dialButtonSlot
QObject::connect(dialButton, SIGNAL(dial(QString)), &dialButtonSlot, SLOT(dial(QString)));

这样当button点击之后被定义为slot的dial方法会被调用,我们就可以对拿到的数据进行处理。

那么如果我们数据处理完了想更新ui怎么通知呢,同样用信号和槽的形式。

  1. 头文件中定义一个signal

    1
    2
    signals:
    void setTextField(QVariant text);
  2. QML中定义一个函数签名一样的方法

    1
    2
    3
    function setTextField(text){
    console.log("setTextField: " + text)
    }
  3. 同样将信号和槽连接起来

    1
    QObject::connect(&handleTextField, SIGNAL(setTextField(QVariant)),window, SLOT(setTextField(QVariant)));
  4. 在我们需要通知QML的地方发送信号即可

    1
    emit setTextField("666666");

QQuickImageProvider

QQuickImageProvider是某种用来存取图片的类。具体使用

  1. 新建一个类继承QQuickImageProvider,覆写requestImage方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class SnapshotImageProvider:public QQuickImageProvider
    {
    public:
    SnapshotImageProvider();
    QImage requestImage(const QString &id, QSize *size, const QSize& requestedSize) override;
    void setSnapshotObject(HandleSnapshotId *handle){
    snapshotObject = handle;
    }

    private:
    QImage screenshot;
    HandleSnapshotId *snapshotObject;
    QQuickWindow *m_window;
    };
1
2
3
4
5
6
7
8
9
QImage SnapshotImageProvider::requestImage(const QString &id, QSize *size, const QSize& requestedSize){
if (id == "fetch") {
qDebug() << "id :"<< snapshotObject->id;
return screenshot;
} else {
screenshot = ........
}

return screenshot;
  1. QML中给Imaged的source属性赋值为QQuickImageProvider。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Image {
    id: snapshotImage
    objectName: "imageObject"
    cache: false;
    x: 23
    y: 259
    width: 325
    height: 445
    source: "image://snapshotimageprovider/presentation"
    }
  2. 把QQuickImageProvider添加到QQmlApplicationEngine中,在添加的时候传入的第一个参数表示这个QQuickImageProvider的名字,在用的时候也必须用这个名字,不过不区分大小写。

    1
    engine.addImageProvider("snapshotimageprovider",imageProvider);

QML在加载这个source的时候会主动去调用requestImage方法,并且他的第一个参数就是source属性中表示QQuickImageProvider名字后面的子串,如上代码,那么在requestImage方法中传入的第一个参数就是"presentation",可以根据这个id进行不同的图片处理。

如果想主动刷新那么只需要重新赋值source即可,但是需要先赋为空

1
2
snapshotImage.source = ""
snapshotImage.source = "image://snapshotimageprovider/presentation"

In the end

当然这边只是Qt的冰山一角,不过用来做Demo的app是足矣了,不知道后续还有没有机会再用Qt开发,其实Qt也只是界面可以大部分跨平台开发,很多其他功能并不是很适用,例如Qt原生的AudioRecorder很多参数不支持set,像bitrate等等,到最后还是导入了mac自带的lib然后用宏区分平台来实现录音功能。因此如果只是简单界面的测试app用qt是可以的,如果涉及的模块较多就不太合适了。

参考资料:https://andrew-jones.com/blog/qml2-to-c---and-back-again-with-signals-and-slots/

bash_profile for mac

之前还在ODM的时候用的是Linux系统,关于环境配置有两个脚本文件经常修改,一个是bash_rc和/etc/profile,bash_rc是针对当前用户的,而/etc/profile是针对所有用户的。

不过这次的topic是关于macOS环境下的。相对于Linux,mac下并没有bash_rc,取而代之的是bash_profile。

bash_profile是在login shell(在填写username和password的时候启动的shell)启动的。至于bash_rc在mac上是没有的,如果习惯使用这个的话就需要在bash_profile中source ~/.bash_rc

1
2
3
if [ -r ~/.bashrc ]; then
source ~/.bashrc
fi

或者这样写更短

1
[ -r ~/.bashrc ] && . ~/.bashrc

和Linux一样,mac下也有/etc/profile。

1
2
3
4
5
6
7
8
9
10
11
File /etc/profile

# System-wide .profile for sh(1)

if [ -x /usr/libexec/path_helper ]; then
eval `/usr/libexec/path_helper -s`
fi

if [ "${BASH-no}" != "no" ]; then
[ -r /etc/bashrc ] && . /etc/bashrc
fi

可以看到是去执行了/etc/bashrc

1
2
3
4
5
6
7
8
9
10
11
File /etc/bashrc

if [ -z "$PS1" ]; then
return
fi

PS1='\h:\W \u\$ '
# Make bash check its window size after a process completes
shopt -s checkwinsize

[ -r "/etc/bashrc_$TERM_PROGRAM" ] && . "/etc/bashrc_$TERM_PROGRAM"

又去执行了bashrc_Apple_Terminal文件,而在这个文件中执行了bash_profile。

因此具体的执行顺序是

  • /etc/profile
    • /etc/bashrc
      • /etc/bashrc_Apple_Terminal
  • if it exists: ~/.bash_profile
    • when ~/.bash_profile does not exists, ~/.bash_login
    • when neither ~/.bash_profile nor ~/.bash_login exist, ~/.profile
  • ~/bash_profile can optionally source ~/.bashrc

Tips:Oh-My-Zsh是mac中非常好用的针对Z-shell的框架,但是他不会自动执行bash_profile,需要在~/.zshrc中添加[ -r ~/.bash_profile ] && . ~/.bash_profile

Screenshot

虽然没做过android screenshot,但是一直以为调用某个系统提供的api,今天搜索了一下才知道各个版本之间的screenshot方式还不太一样。

4.0以下采用JNI : http://blog.csdn.net/zmyde2010/article/details/6925498

4.0~4.2采用反射方法获取截屏api :http://blog.csdn.net/cjd6568358/article/details/39120037

4.3~4.4 api被hide标签修饰,需要root,执行adb shell /system/bin/screencap -p /sdcard/screenshot.png

5.0以上则主要用MediaProjection来实现。

其实也不算很复杂,用MediaProjectionManager的createScreenCaptureIntent方法构建出一个intent并且发送出去就行。

1
2
3
void startScreenshot() {
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);
}

查看源码其实是直接启动一个权限相关的Activity弹框,实际显示的时候也会弹框出现。当用户允许了之后就会开始截屏操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Intent createScreenCaptureIntent() {
Intent i = new Intent();
final ComponentName mediaProjectionPermissionDialogComponent =
ComponentName.unflattenFromString(mContext.getResources().getString(
com.android.internal.R.string
.config_mediaProjectionPermissionDialogComponent));
i.setComponent(mediaProjectionPermissionDialogComponent);
return i;
}

<string name="config_mediaProjectionPermissionDialogComponent" translatable="false">
com.android.systemui/com.android.systemui.media.MediaProjectionPermissionActivity
</string>

当然这边只是启动screenshot,还需要拿到screenshot的数据。既然是startActivityForResult,那肯定是在onActivityResult方法中拿到数据的。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_MEDIA_PROJECTION) {
resultData = data;
this.resultCode = resultCode;
}
mediaProjection = mediaProjectionManager.getMediaProjection(this.resultCode, resultData);
mediaProjection.createVirtualDisplay("Screenshot", surfaceView.getWidth(),
surfaceView.getHeight(), screenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mSurface, null, null);
}

只要能正确实例化MediaProjection就可以拿到screenshot的数据了,这边是直接将其显示在surface上,如果希望保存下来的话需要实例化一个ImageReader。

1
2
3
4
5
6
7
8
9
10
11
12
ImageReader imageReader = ImageReader.newInstance(displayWidth, displayWidth, PixelFormat.RGBA_8888, 2);
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
......
//Get screenshot
image = reader.acquireLatestImage();
Bitmap bitmap = convertImagetoBitmap(image);
......
imageView.setImageBitmap(bitmap);
}
}, null);

不过,对比一下Qt下的截屏,Android真的是太复杂了,而且暂时也想不到有什么跨平台的方式能实现Android平台的screenshot。

1
2
3
4
5
int screenNumber = QApplication::desktop()->screenNumber(QCursor::pos());
QScreen* screen = QApplication::screens().at(screenNumber);
QRect screenGeometry = screen->geometry();
QPixmap pixmap = screen->grabWindow(0, screenGeometry.x(), screenGeometry.y(), screenGeometry.width(), screenGeometry.height());
QImage screenshot = pixmap.toImage();

总结

  1. 发送intent

    1
    startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);
  2. 覆写onActivityResult,在方法中拿到实例化的MediaRejection

    1
    2
    3
    4
    5
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    ......
    mediaProjection = mediaProjectionManager.getMediaProjection(this.resultCode, resultData);
    }
  3. 如果要显示在SurfaceView上,就传入SurfaceView的Surface,如果要保存数据则传入imageReader.getSurface()

    1
    2
    3
    4
    5
    6
    mediaProjection.createVirtualDisplay("Screenshot", surfaceView.getWidth(),
    surfaceView.getHeight(), screenDensity,
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
    //surfaceView.getHolder().getSurface()
    //imageReader.getSurface()
    mSurface, null, null);

Pass by value or reference

Given a sorted linked list, delete all duplicates such that each element appear only once.
Example 1:

1
2
Input: 1->1->2
Output: 1->2

Example 2:

1
2
Input: 1->1->2->3->3
Output: 1->2->3

解题上并不难,移动的节点时候判断与后一个是否一样,如果一样就跳过。

1
2
3
4
5
6
7
8
9
10
11
public static ListNode deleteDuplicates(ListNode head) {
ListNode current = head;
while (current != null && current.next != null) {
if (current.next.val == current.val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}

不过在main函数里执行的时候,我发现获取最终结果,不需要使用该方法return的ListNode,直接将原链表作为参数传入,方法执行完之后原链表就发生了变化。

1
2
3
//Same code
deleteDuplicates(first);
ListNode resultNode = deleteDuplicates(first);

这突然让我有点怀疑java究竟是pass by value还是reference。Java中最突出的特点之一就是没有指针。如果都是按值传递为什么原链表会发生变化呢。最经典的swap说明了Java的按值传递性。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int i = 10;
int j = 20;
swap(i, j);
System.out.println("i :" + i);
System.out.println("j :" + j);
}

public static void swap(int i, int j) {
int tmp = i;
i = j;
j = tmp;
}

1
2
i : 10
j : 20

但是下面这个例子好像又表示pass by reference

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Dog mDog = new Dog("Rover");
foo(mDog);
System.out.println(mDog.getName());
}

public static void foo(Dog someDog) {
someDog.setName("Max1"); // AAA
someDog = new Dog("Fifi"); // BBB
someDog.setName("Rowlf"); // CCC
}

1
Max1

也就是说Java在传递参数的时候对基础类型和非基础类型的处理是不同的。基础类型的话是完全字面意义上的pass-by-value。而非基础类型的话,其实是创建了一个新的对象newObject(空对象),然后将这个newObject的引用指向传入的参数。如图中所示,图片来源:https://stackoverflow.com/questions/9404625/java-pass-by-reference/9404727#9404727

总结

在Java中,确实一切都是pass by value。但是拷贝是引用还是变量取决于源数据类型。

  1. 如果是基础类型,那么参数是pass by value。
  2. 如果是对象,那么对象的引用是pass by value。
  3. 在方法内部修改对象的引用是不会影响原引用的。
  4. 在方法内部修改collection和map类型会影响原数据。

Class File Format

概述

Class文件是一组以8位字节为基础单位的二进制流,可以用Hex Friend等工具打开。
CLass结构:

  • 无符号数:基本类型,u1,u2,u4,u4分别代表一个字节、两个字节、四个字节、八个字节的无符号数。
  • 表:多个无符号数或者其他表作为数据项构成的复合数据类型,习惯以_info结尾,整个class文件本质上就是一张表。

class具体结构

魔数

每个class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,大部分文件存储标准都是通过魔数来进行身份验证的。因为扩展名可以随意改动。对Class文件来说魔数值为0xCAFEBABE

版本号

在魔数之后的4个字节存储的是class文件的版本号,前两个字节是次版本号,后两个是主版本号。Java版本号是从45开始的,例如JDK1.1能支持版本号45.0 ~ 45.65535的Class文件,JDK1.2则能执行45.0 ~ 46.65535的Class文件。这边示例的class文件是用JDK11,因此应该是55。

常量池

常量池:常量池可以理解为class文件中的资源仓库,它是class文件结构中与其他项目关联最多的数据类型。

由于一个类中有多少方法和变量是不确定的,因此常量池入口有一项u2类型的数据,代表常量池容量计数值。不过这个容量计数是从1开始的。也就是说如果这个值是57,那么就有56个常量,之所以把0空出来是因为如果某个常量的索引不引用任何一个常量就可以用0来表示。图中的容量计数值为59,因此一共有58个常量。

常量池主要存放两大类常量:字面量和符合引用

  • 字面量:Java语音的常量概念,final修饰的关键字,字符串等等
  • 符号引用:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

常量池中的常量每一个都是一个表,并且表的结构数据不同,有的是三列,有的是两列,为了区分它们的结构,这些表的第一位是一个u1类型的标志位,代表属于哪种表的类型也就是说代表哪一种常量类型。例如如果是1那么表示是utf8类型的常量,如果是10那么就是方法的符号引用。

查看下第一个常量是0A,也就是10,10对应常量池中的项目类型是CONSTANT_Methodref_info,而这个类型一共有三个参数,第一个是tag,也就是10,第二个和第三个都是u2类型,如图中是0E(14)和1B(27),分别表示指向声明方法和指向名称的索引。

可以使用javap反编译来确认下。第一个常量是这样的,和用hex friend计算出来是一样的。

1
#1 = Methodref          #14.#27

访问标志

常量池之后是access_flags,这个标志用于识别一些类或者接口层次的访问信息,例如这个class是类还是接口,是public还是abstract等等。使用javap也可以看到标志。

1
flags: (0x0021) ACC_PUBLIC, ACC_SUPER

类索引、父类索引、接口索引集合

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。Class文件中由这三项来确定继承关系。

类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过这个类描述符常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

例如图中this_class的索引是13,而13的索引又指向了55,55则代表这个类名。

字段表集合

field_info用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,不包括方法中的临时变量。

字段表的结构用access_flags来表示作用域、是否final、是否static等等,另外用了两个索引来表示这个变量的名称和类型(有映射的字符,例如int对应I)。另外还有两个字段存储额外的信息,如果我们给一个变量赋了初始值,那么这两个字段就会有对应的值。

方法表集合

类似于字段表集合,由于方法没有volatile和transient关键字,因此access_flags中没有ACC_VOLATILE标志和ACC_TRANSIENT标志。而增加了synchronized、native等修饰方法的关键字。由于和字段表集合大同小异,不多赘述。

但是需要注意的是方法中的代码被单独存放在了方法表中的code字段下,可以看到下图是用javap反编译出的main方法。access_flags是ACC_PUBLIC和ACC_STATIC,descriptor表示传入的参数和返回值。code字段下是方法中的代码。

Maximum Subarray

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

Example:

1
2
3
Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

先来一个暴力穷举法:

1
2
3
4
5
6
7
8
9
10
int sum = 0;
int max = nums[0] > 0 ? 0 : nums[0];
for (int i = 0; i < nums.length; i++) {
for (int j = i; j < nums.length; j++) {
sum += nums[j];
max = Math.max(sum, max);
}
sum = 0;
}
return max;

这里是从0 ~ i递增的方式累加,重新换种递减的方式

1
2
3
4
5
6
7
8
9
10
int sum = 0;
int max = nums[0] > 0 ? 0 : nums[0];
for (int i = 0; i < nums.length; i++) {
for (int j = i; j >= 0; j--) {
sum += nums[j];
max = Math.max(sum, max);
}
sum = 0;
}
return max;

在累加的过程中可以把这个内层的循环优化一下。例如有个数组{1,2,3,-5,4}

1
2
3
4
5
1 + 2 + 3 + -5 + 4 =  (1 + 2 + 3 + -5) + 4
1 + 2 + 3 + -5 = (1 + 2 + 3) + -5
1 + 2 + 3 = (1 + 2) + 3
1 + 2 = (1) + 2
1 = 1

如果抽象一下的话就是

1
sum(i) = A[i] + sum(i - 1)

那么如果是求之前总和的最大值,就是比较一下A[i] + sum(i - 1) 和 A[i]或者也可以说是sum(i - 1)是否大于零

1
maxSum(i) = max(A[i], A[i] + sum(i - 1))

因此优化后的代码为

1
2
3
4
5
6
7
8
int max = nums[0];
int maxSum[] = new int[nums.length];
maxSum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
maxSum[i] = Math.max(nums[i], maxSum[i - 1] + nums[i]);
max = Math.max(maxSum[i], max);
}
return max;

由于maxSum的数组不是必要的,可以用一个变量来代替。

1
2
3
4
5
6
7
 int max = nums[0];
int maxSum = nums[0];
for (int i = 1; i < nums.length; i++) {
maxSum = Math.max(nums[i], maxSum + nums[i]);
max = Math.max(maxSum, max);
}
return max;

参考文章:http://theoryofprogramming.com/2016/10/21/dynamic-programming-kadanes-algorithm/

Garbage Collection

如果判断对象已死

引用计数算法

给对象添加一个引用计数器,每当有个地方引用他时计数器就加1,引用失效时,计数器就减1,当计数器为0时那么该对象则已经死亡。不过该算法不能解决对象之间循环引用的问题。但是也有优点那就是因为实现简单因此判定效率很高。

1
2
3
4
5
6
7
8
ObjectA A = new ObjectA();
ObjectB B = new ObjectB();
A.instance = B;
B.instance = A;

//如果使用引用计数,即使赋值为null也无法回收,因为互相引用,计数器不会为0
A = null;
B = null;

可达性分析算法

基本思想是通过一系列称为GC Roots的对象作为起始点,从这些节点往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,那么这个对象就是不可用的。

GC Roots最大的特点就是它一定不会被回收,以虚拟机栈举例,如果栈中有个对象A引用了对象B,如果B没有引用其他对象,那么以A为起始点的可达性分析就结束了,并且A和B都不会回收。

可作为GC Roots的对象包括以下:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

对象死亡过程

要真正宣告一个对象死亡,至少需要两次标记过程:

  1. 如果对象经过可达性分析之后没有发现任何和GC Roots相连接的引用链,那么会进行第一次标记并进行一次筛选,筛选条件为是否需要执行对象的finalize方法,当对象没有覆盖或者已经执行过finalize,就不会再执行finalize。
  2. 如果确实需要执行finalize方法,这个对象会被放在F-Queue队列中,之后被一个由虚拟机自动建立的Finalizer线程去执行。如果finalize方法中仍然没有和引用链上的对象建立连接,GC会对F-Queue进行第二次标记,然后彻底进行回收。

Warning:由于finalize方法运行代价高昂,不确定性大,因此不建议复写该方法,如果有收尾的处理放在try-finally中更好。

垃圾收集算法

标记-清除算法

顾名思义,一共分为两个阶段,标记出所有需要回收的对象(@对象死亡过程),完成标记之后统一回收所有的对象。

该算法有两个缺点:1.效率不高 2.标记清除之后会产生大量不连续的内存碎片,碎片太多会导致以后需要分配较大对象时,由于连续内存不够需要提前触发一次GC。

复制算法

基本思想是将可用内存等分为两个部分,每次分配内存都只从其中一个分配,当A内存用完了,将A内存中还存活的对象全部复制到B内存上去,再把A内存一次清理掉。这样能解决内存碎片的问题,但是代价则是将内存缩小为原来的一半。但是没有必要严格按照1:1的比例来切割,因为新生代中的对象98%都是很快就回收了的。

标记-整理算法

标记整理算法和标记清除算法的标记过程是一样的。但在清理之前多了一步,就是将所有存活的对象向一端移动。

由于复制算法在对象存活率较高的老年代要复制很多对象,该算法适用于老年代。

分代收集算法

根据对象存活周期的不同将内存划分为几块,一般为新生代和老年代。新生代由于大批对象死亡,因此使用复制算法,而老年代由于存活率高,就需要用标记清除或者标记整理的算法。

Stop The World:GC进行时必须暂停Java的所有线程。

垃圾收集器

垃圾收集器是收集算法的具体实现

Serial收集器

该收集器是单线程收集器,只使用一个CPU或者线程区完成GC。因此优点在于简单高效,然而缺点也很明显它在进行垃圾收集时,必须暂停其他所有的工作线程。

ParNew收集器

该收集器是Serial收集器的多线程版本,除了使用多线程之外,其余的例如收集算法、Stop The World、回收策略等等都和Serial收集器完全一样。

它作为Server模式下的虚拟机中首选的新生代收集器原因之一是只有它能和CMS收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器。Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。可以通过设置收集器的参数来控制吞吐量。

吞吐量:CPU用于运行代码的时间占比,如果虚拟机一共运行了100分钟,GC了10分钟,那么吞吐量就是90%。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本。同样单线程,采用标记整理算法。

Parallel Old收集器

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

CMS收集器

Concurrent Mark Sweep从名字可以看出这是一个并发的收集器,以获取最短回收停顿时间为目标的收集器。并且基于标记清除算法。不过有以下三个缺点

  • 由于是并发,会在一定程度上占用CPU。
  • 无法处理浮动垃圾,因为在GC过程中没有停顿,因次会有新的对象生成。
  • 由于是标记清除算法,因此会有大量内存碎片生成。

内存分配

  • 新生代
    • Edan区
    • 两个Survivor区
  • 老年代
  • 永久代

  • Minor GC: 在新生代发生的垃圾收集动作,较频繁,时间短

  • Major GC/Full GC: 在老年代发生的GC,速度较慢。

大多数情况,新建的对象将分配到Edan区,当Edan区空间不足时,就会触发一次Minor GC,也就是将Edan区清空,然后将存活的对象拷贝到Survivor区,并将Survivor区内对象的年龄默认设置为1,之后每经过一次GC就会增长1,并且会在两个Survivor区之间互相切换。当成长到15的时候就会被转移到老年代。当然这个阀值可以通过参数设置。

至于为什么需要两个Survivor区,假设只有一个Survivor区,那么第一次执行GC,将存活的对象拷贝到Survivor区,暂时没毛病,程序运行了一段时间又满了,这时候再执行GC,但是在这种情况下对Survivor区执行GC的话就会在Survivor区内造成大量的内存碎片,因此需要两个Survivor区的主要原因是为了清除生成的碎片,能保证有一块Survivor永远处于干净状态。另一个原因是可以继续对Survivor区进行复制的垃圾收集算法,也是为了提升GC的性能。

对于某些大对象将直接进入老年代,大对象就是指需要大量连续内存的对象,例如长字符串或者数组。

图解:http://www.tothenew.com/blog/java-garbage-collection-an-overview/

JVM中的内存与对象

虚拟机内存的各个区域

按照私有和共享对区域进行区分

  • 私有
    • 程序计数器:当前线程所执行的字节码的行号指示器。在Java虚拟机里面,字节码解释器就是通过这个行号来读取下一条要运行的指令,也可以说是记录了正在执行的虚拟机字节码指令的地址。在线程切换还有基础的逻辑(if、循环、switch)都需要用到这个程序计数器。每条线程都有一个独立的程序计数器。
    • 虚拟机栈:每个方法在创建的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、方法出口等。也就是说当我们调用某个方法,那么就会有个栈帧入栈,当执行完成了,该栈帧就会出栈。也可把虚拟机栈叫做局部变量表,存放了基本数据类型和reference类型,returnAddress类型。
    • 本地方法栈:与虚拟机栈非常类似,不过本地方法栈执行的是native方法。
  • 共享
    • 堆:被所有线程共享的内存区域,唯一的目的就是存放对象实例,也是GC作用的主要区域。
    • 方法区:存储已被虚拟机加载的类信息、常量、静态变量等数据。
      • 运行时常量池:存放编译期生成的各种字面量和符号引用。也就是加载类的各种描述信息。

对象的创建

  1. 检查常量池中是否有该类的符号引用。并查看该类是否被加载、初始化,如果没有那么需要先执行类加载过程。
  2. 为对象分配一块确定大小的内存,有两种方式,根据堆是否规整来决定,决定堆是否规整是由采用的GC方式是否带有压缩整理功能。

    • 指针碰撞:如果Java堆中内存是绝对规整的,那么会将堆分成两部分,一部分是已经使用的内存,一部分是未使用的内存,分界线就是指针,如果需要分配内存那么移动指针即可。
    • 空闲列表:如果不是绝对规整的,那么就需要创建两个列表来维护。

      需要考虑内存的分配是否有线程安全问题。一个方案是进行同步处理,采用CAS配上失败重试的方法来保证更新操作的原子性。另一种方案则是每个线程在堆中预先分配一小块内存。称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程需要创建对象,先从TLAB上分配,当TLAB分配光了,再用同步的方式分配新的TLAB。

  3. 分配完内存之后,将分配到的内存空间都初始化为零值。保证对象的实例字段可以不被赋值就直接使用。

  4. 设置一些信息,例如类的元数据信息,哈希码,GC分代年龄等等。
  5. 执行init方法,按照我们的意愿进行初始化。

对象的内存布局

对象的内存布局主要有三块:对象头、实例数据、对齐填充。

对象头:

第一部分为Mark Word,用于存储对象自身的运行时数据,例如哈希码、GC分代年龄、线程持有的锁等等。由于对象头信息与对象自身的数据没有关系,因此属于额外成本,因为被设计成非固定的数据结构以便能用最小的空间存储尽量多的信息。

第二部分为类型指针,就是对象指向它的类元数据的指针,通过这个指针能知道这个这个对象是哪个类的实例。

实例数据:该数据为对象存储的有效信息。也就是代码中所写的一切,包括父类继承的或者子类中定义的各种信息。

对齐填充:因为HotSpot VM的自动内存管理系统要求对象的大小必须是8字节的整数倍,对象头部分刚好是8字节的整数倍,如果实例数据不是8字节的整数倍,需要通过对齐填充部分进行补齐操作。

装饰器

以奶茶店为例子,如果有红茶和绿茶两种,那么应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class milktea {
String description;

public String getDescription() {
return description;
}

void show() {
System.out.println(getDescription());
}
}

class greenMilktea extends milktea {
public greenMilktea() {
description = "green milktea";
}
}

class redMilktea extends milktea {
public redMilktea() {
description = "red Milktea";
}
}

都知道奶茶中可以加各种奇奇怪怪的东西,像燕麦,珍珠之类的,如果要加这些东西该怎么写比较好呢。

第一种将所有能加的东西排列组合,并增加对应的类

  • GreenMilkTeaWithPearl
  • GreenMilkTeaWithSugar
  • GreenMilkTeaWithSugarAndPearl
  • RedMilkTeaWithPearl
  • RedMilkTeaWithSugar
  • RedMilkTeaWithSugarAndPearl

这才只有两种配料,因此这种方式肯定不可取。

第二种添加成员属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Milktea {
//配料
boolean addSugar;
boolean addPearl;
boolean addIce;

String description;

public String getDescription() {
return description;
}

void show() {
System.out.println(getDescription());
}
}

这种相对好一点但是也有很多冗余的代码。其实最好的方式还是按照奶茶店的模式来,将红茶和绿茶作为饮品,而珍珠、糖之类的作为配料单都拿出来。也就是先把茶给制作出来,然后在对这个茶加不同的配料或者是对这个奶茶进行装饰。

那么我们做茶的配方(代码)是没有变的。我们需要增加配料就可以了。这里加了冰激淋和糖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Icecream extends Ingredients {
Milktea mt;

public Icecream(Milktea mt) {
this.mt = mt;
}

@Override
public String getDescription() {
return mt.getDescription() + " add Icecream";
}
}

class Sugar extends Ingredients {
Milktea mt;

public Sugar(Milktea mt) {
this.mt = mt;
}

@Override
public String getDescription() {
return mt.getDescription() + " add Sugar";
}
}

有个疑问为什么要传入奶茶对象呢,因为当然要知道冰激凌要加在哪一杯奶茶中啊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
//没有任何配料的奶茶
Milktea mt = new GreenMilktea();
mt.show();

//制作一杯红茶
Milktea mt2 = new RedMilktea();
//加冰淇淋
mt2 = new Icecream(mt2);
//加糖
mt2 = new Sugar(mt2);
//交给客户
mt2.show();
}

1
2
3
Result:
green Milktea
red Milktea add Icecream add Sugar

看起来就像是把一个对象作为参数传递进去并包装了一下,通过层层嵌套的形式将一个对象包装起来。如果又来一个新配料也好办,使用方式都是一样的,新增一个类,将想要装饰的类传进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
......
psvm {
mt2 = new Pearl(mt2);
}


class Pearl extends Ingredients {
Milktea mt;

public Pearl(Milktea mt) {
this.mt = mt;
}

@Override
public String getDescription() {
return mt.getDescription() + " add Pearl";
}
}

总结

好处:

  • 完全不用修改原有的代码,容易扩展新的功能。
  • 原始类可以非常简单,之后根据需要对原始类进行包装。

坏处:

  • 由于是嵌套的初始化形式,因此看起来很复杂。如果层数多了跟代码会特别困难。

不管怎么说设计模式这种东西一定不能生搬硬套,而是要灵活应用。

Delegation

类代理

面向对象的特性中由于继承的存在关系,因此当扩展一个类并自行重新定义一些细节的时候,代码就会变的依赖于这个子类,而如果后期父类更新了,那么子类的实现可能与父类背道而驰,因此kotlin中默认将类修饰为final。但有的时候确实需要添加一些特定情况下才发生的新功能。

因为在接口中定义的方法必须在实现类中全部实现,因此如果我们只是在原有的类上加一些新功能,那么会导致有很多重复的代码。

在kotlin中可以通过by省略掉很多不需要的代码。如下代码中的test方法不用重写直接使用代理,也就是circle的test方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fun main(args: Array<String>) {
val redcircle = RedCircle(Circle())
redcircle.draw()
redcircle.test()
}

interface Shape {
open val descpition: String
fun draw()
fun test()
}

class Circle : Shape {
open override val descpition: String
get() = "Circle"

override fun draw() {
println("draw $descpition")
}

override fun test() {
println("test")
}
}

class RedCircle(circle: Circle) : Shape by circle {
override fun draw() {
println(Circle().descpition + " is red")
}

}

1
Result:circle is red

当然因为这里类比较简单,如果非常多的话例如collection接口,要实现五个方法,然而我们只是想新增一个方法,就会显得代码非常冗余

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyCollection :Collection<String>{
override val size: Int
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.

override fun contains(element: String): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun containsAll(elements: Collection<String>): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun isEmpty(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun iterator(): Iterator<String> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

照例还是看下代理模式的java源码,这边只关注redCircle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class redCircle implements Shape {
// $FF: synthetic field
private final circle $$delegate_0;

public void draw() {
String var1 = (new circle()).getDescpition() + " is red";
System.out.println(var1);
}

public redCircle(@NotNull circle circle) {
Intrinsics.checkParameterIsNotNull(circle, "circle");
super();
this.$$delegate_0 = circle;
}

@NotNull
public String getDescpition() {
return this.$$delegate_0.getDescpition();
}

public void test() {
this.$$delegate_0.test();
}
}

可以看到test方法由系统自动生成,并且是通过调用传入的circle对象来实现的。

像kotlin的by关键字非常类似于java中的装饰器。本质上都是将原始类作为属性传入并保存,如果想保留原有的设计,那么直接调用原始类的方法即可。